{
 "openapi": "3.1.0",
 "info": {
  "title": "Formatly File Conversion API",
  "version": "1.0.0",
  "summary": "Privacy-first file conversion for images, audio, video and documents.",
  "description": "Convert files from your code. Privacy-first: EU-hosted (Hetzner, Germany), every uploaded and converted file is deleted within 1 hour, and content is never used to train AI models.\n\n**Flow (async):**\n1. `POST /api/v1/convert` with a file and a target format → returns a `jobId` and the credit cost.\n2. Poll `GET /api/v1/jobs/{jobId}` until `status` is `done`.\n3. Download from the `downloadUrl` in the job response (valid 15 minutes).\n\n**Auth:** every request needs `Authorization: Bearer ck_live_…`. Generate a key in My Account after verifying your email — available on every plan, including Free.\n\n**Credits:** one monthly pool shared by the web app and the API, weighted by file size + type — 1 credit for an image, 3 for audio/documents/files >10 MB, 10 for video/PDF/Office/files >100 MB, then 25 / 50 / 100 for files over 500 MB / 1 GB / 2 GB. Included monthly: Free 250 · Pro 3,000 · Business 25,000. Every response includes your remaining credits. Add a card to enable pay-as-you-go (€5 per 1,000 credits) past your allowance.",
  "contact": {
   "name": "Formatly Support",
   "email": "support@formatly.pro",
   "url": "https://formatly.pro/api.html"
  }
 },
 "servers": [
  {
   "url": "https://formatly.pro",
   "description": "Production"
  }
 ],
 "security": [
  {
   "bearerAuth": []
  }
 ],
 "tags": [
  {
   "name": "Convert",
   "description": "Single-file conversion and tool operations."
  },
  {
   "name": "Jobs",
   "description": "Poll job status and obtain the download URL."
  },
  {
   "name": "Multi-file",
   "description": "Merge PDFs and batch-convert many files at once."
  },
  {
   "name": "Account",
   "description": "Credit balance and conversion history."
  },
  {
   "name": "Extract",
   "description": "Data-in/data-out tools — return structured JSON, not a converted file, so they have their own poll endpoints. All run on-box in the EU with no external AI; input and output deleted within 1 hour."
  }
 ],
 "paths": {
  "/api/v1/convert": {
   "post": {
    "tags": [
     "Convert"
    ],
    "summary": "Convert one file (or run a tool operation)",
    "description": "Upload a single file and convert it to `to`, or run an in-place tool by setting `op`. Returns a `jobId`; poll `GET /api/v1/jobs/{jobId}` for the result. Credits are charged on acceptance and refunded automatically if the job fails to start.",
    "operationId": "convertFile",
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "file",
         "to"
        ],
        "properties": {
         "file": {
          "type": "string",
          "format": "binary",
          "description": "The file to convert. Max size by plan: Free 25 MB · Pro 500 MB · Business 5 GB."
         },
         "to": {
          "type": "string",
          "description": "Output format (lower-case extension). Images: jpg, png, webp, avif, gif, tiff, ico. Audio: mp3, wav, ogg, aac, flac, m4a, opus, aiff. Video: mp4, mov, avi, webm, gif. Documents: pdf, docx (from PDF or legacy .doc), pptx (from .ppt), xlsx (from .xls/.csv/.json), txt, epub, odt, ods, odp, rtf, csv. Data & subtitles: json (from csv/xls/xlsx), vtt (from srt), srt (from vtt). For an in-place tool (e.g. compress, ocr, redact) set `op` and `to` to the same format. See https://formatly.pro/api.html for the full matrix.",
          "examples": [
           "jpg",
           "mp3",
           "pdf",
           "mp4"
          ]
         },
         "op": {
          "type": "string",
          "description": "Optional tool operation (output keeps the input format unless noted). Omit for a plain format conversion. These are the single-file tool operations. The remaining premium tools have their own endpoints: PDF merge (POST /api/pdf/merge), CV → Spreadsheet (POST /api/parse-cv), transcription (POST /api/transcribe), file search (POST /api/file-search) and PDF → Markdown (POST /api/pdf-to-markdown). ProRes/DNxHR export is a format conversion — pass it as `to`, not `op`.",
          "enum": [
           "clean-metadata",
           "compress",
           "resize",
           "compress-video",
           "blur-faces",
           "remove-bg",
           "ocr",
           "remove-watermark",
           "redact",
           "repair-pdf",
           "repair-csv",
           "repair-office",
           "rotate-pdf",
           "split-pdf",
           "grayscale",
           "invert",
           "sepia",
           "compress-pdf",
           "pdf-to-text",
           "extract-images",
           "extract-pages",
           "delete-pages",
           "protect-pdf",
           "unlock-pdf",
           "mute-video",
           "trim-media",
           "normalize-audio"
          ]
         },
         "quality": {
          "type": "integer",
          "minimum": 10,
          "maximum": 95,
          "description": "Quality for `compress` / `compress-video` / `resize` (10–95). Higher = larger file, better quality."
         },
         "angle": {
          "type": "integer",
          "enum": [
           90,
           180,
           270
          ],
          "description": "Clockwise rotation for `op=rotate-pdf`."
         },
         "pages": {
          "type": "string",
          "description": "Page selection for `op=extract-pages` / `delete-pages`, e.g. \"1-3, 5\".",
          "example": "1-3, 5"
         },
         "split": {
          "type": "string",
          "description": "Page-range spec for `op=split-pdf`; returns a ZIP of PDFs."
         },
         "password": {
          "type": "string",
          "description": "Password for `op=protect-pdf` (set) or `unlock-pdf` (current). Validated for length only; never logged."
         },
         "lang": {
          "type": "string",
          "description": "Document language for `op=ocr` — one of eng, fra, deu, jpn, or up to three joined with \"+\" (e.g. \"eng+fra\"). Default: eng.",
          "examples": [
           "fra",
           "eng+fra"
          ]
         },
         "trim": {
          "type": "string",
          "description": "JSON object for `op=trim-media`: {\"start\": seconds, \"end\": seconds}."
         },
         "resize": {
          "type": "string",
          "description": "JSON object for `op=resize`: {\"width\": px, \"height\": px, \"fit\": \"cover|contain|inside|fill\"}."
         },
         "redact": {
          "type": "string",
          "description": "JSON object for `op=redact` (PDF): {\"patterns\": [\"email\",\"phone\",\"ssn\",\"credit-card\",\"iban\"], \"terms\": [\"exact phrase\"]}. Matched text is removed from the content stream, not just covered."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Job accepted and queued.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ConvertAccepted"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/OutOfCredits"
     },
     "413": {
      "description": "File exceeds the plan's size limit.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "429": {
      "$ref": "#/components/responses/RateLimited"
     }
    }
   }
  },
  "/api/v1/jobs/{jobId}": {
   "get": {
    "tags": [
     "Jobs"
    ],
    "summary": "Get job status + download URL",
    "description": "Poll until `status` is `done`, then fetch `downloadUrl` (valid 15 minutes; reusable within that window). A `failed` status includes an `error`.",
    "operationId": "getJob",
    "parameters": [
     {
      "name": "jobId",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Job status.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/JobStatus"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "description": "Job not found or not owned by this key.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/download/{token}": {
   "get": {
    "tags": [
     "Jobs"
    ],
    "summary": "Download a converted file",
    "description": "Fetch the converted file using the opaque token from a job's `downloadUrl` (or a signed URL). Returns the file bytes with a `Content-Disposition` filename.",
    "operationId": "downloadFile",
    "security": [],
    "parameters": [
     {
      "name": "token",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The converted file.",
      "content": {
       "application/octet-stream": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "400": {
      "description": "Invalid token.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     },
     "404": {
      "description": "Expired or already consumed.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Error"
        }
       }
      }
     }
    }
   }
  },
  "/api/pdf/merge": {
   "post": {
    "tags": [
     "Multi-file"
    ],
    "summary": "Merge several PDFs into one",
    "description": "Send 2+ PDFs in the `files` field (repeat the field once per file), in the order you want them merged. Returns a `jobId`; poll `/api/v1/jobs/{jobId}`.",
    "operationId": "mergePdf",
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "Two or more PDF files (field name: files)."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Merge job accepted.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/ConvertAccepted"
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "402": {
      "$ref": "#/components/responses/OutOfCredits"
     }
    }
   }
  },
  "/api/batch/convert": {
   "post": {
    "tags": [
     "Multi-file"
    ],
    "summary": "Batch-convert many files",
    "description": "Convert up to your plan's batch limit of files to the same `to` format (or apply one `op`). Returns a `batchId`; poll `GET /api/batch/{id}` and download a ZIP from `GET /api/batch/{id}/download`.",
    "operationId": "batchConvert",
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files",
         "to"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "Files to convert (field name: files)."
         },
         "to": {
          "type": "string"
         },
         "op": {
          "type": "string"
         },
         "quality": {
          "type": "integer"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Batch accepted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "batchId": {
           "type": "string"
          },
          "total": {
           "type": "integer"
          },
          "errors": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "400": {
      "$ref": "#/components/responses/BadRequest"
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/api/batch/{id}": {
   "get": {
    "tags": [
     "Multi-file"
    ],
    "summary": "Batch status",
    "operationId": "getBatch",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Batch progress + downloadUrl when complete.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "total": {
           "type": "integer"
          },
          "completed": {
           "type": "integer"
          },
          "done": {
           "type": "boolean"
          },
          "downloadUrl": {
           "type": "string",
           "nullable": true
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/api/batch/{id}/download": {
   "get": {
    "tags": [
     "Multi-file"
    ],
    "summary": "Download batch results (ZIP)",
    "operationId": "downloadBatch",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "ZIP of converted files.",
      "content": {
       "application/zip": {
        "schema": {
         "type": "string",
         "format": "binary"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "404": {
      "description": "Not ready or not found."
     }
    }
   }
  },
  "/api/v1/signed-url": {
   "post": {
    "tags": [
     "Jobs"
    ],
    "summary": "Mint a reusable download URL (Business and up)",
    "description": "Available on Business and Enterprise. Returns a reusable signed URL for a completed job's output, valid for a configurable TTL (longer than the default 15-minute single-mint link). Maximum TTL by plan: Business 7 days; Enterprise 30 days.",
    "operationId": "createSignedUrl",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "jobId"
        ],
        "properties": {
         "jobId": {
          "type": "string"
         },
         "expirySeconds": {
          "type": "integer",
          "description": "Seconds; clamped to the plan maximum."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Signed URL.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "url": {
           "type": "string"
          },
          "expiresIn": {
           "type": "integer"
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     },
     "403": {
      "description": "Requires a Business or Enterprise plan."
     }
    }
   }
  },
  "/api/v1/credits": {
   "get": {
    "tags": [
     "Account"
    ],
    "summary": "Credit balance",
    "operationId": "getCredits",
    "responses": {
     "200": {
      "description": "Current credit balance + reset date.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/CreditBalance"
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/api/v1/history": {
   "get": {
    "tags": [
     "Account"
    ],
    "summary": "Conversion history",
    "operationId": "getHistory",
    "parameters": [
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 50
      }
     },
     {
      "name": "offset",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 0
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Recent conversions.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "history": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "$ref": "#/components/responses/Unauthorized"
     }
    }
   }
  },
  "/api/parse-cv": {
   "post": {
    "tags": [
     "Extract"
    ],
    "summary": "Parse CVs into candidate rows",
    "description": "Up to 50 CVs (pdf/docx/doc) into rows: name, email, phone, title, employer, location, linkedin, skills, years_exp, each with a confidence. Bilingual FR/EN. Poll GET /api/parse-cv/{id}.",
    "operationId": "parseCv",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "One or more input files (field name: files)."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Parse batch accepted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "batchId": {
           "type": "string"
          },
          "total": {
           "type": "integer"
          },
          "statusUrl": {
           "type": "string"
          },
          "errors": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Not authenticated."
     },
     "403": {
      "description": "Plan gate: one free try used, or the tool requires a higher tier."
     }
    }
   }
  },
  "/api/parse-cv/{id}": {
   "get": {
    "tags": [
     "Extract"
    ],
    "summary": "Poll a CV parse batch",
    "operationId": "parseCvStatus",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Per-CV rows.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "done": {
           "type": "boolean"
          },
          "total": {
           "type": "integer"
          },
          "completed": {
           "type": "integer"
          },
          "failed": {
           "type": "integer"
          },
          "rows": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "403": {
      "description": "Not your batch."
     },
     "404": {
      "description": "Batch not found or expired (results live 1 hour)."
     }
    }
   }
  },
  "/api/transcribe": {
   "post": {
    "tags": [
     "Extract"
    ],
    "summary": "Transcribe audio/video (Business tier)",
    "description": "Up to 10 media files into transcript + per-segment timestamps (render SRT/VTT). Each file max 2 hours and 2 GB. Optional lang (en/fr/de/es/it/nl/pt/ja). Business tier and up. Poll GET /api/transcribe/{id}.",
    "operationId": "transcribe",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "One or more input files (field name: files)."
         },
         "lang": {
          "type": "string",
          "description": "Optional language hint; omit to auto-detect."
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Transcription batch accepted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "batchId": {
           "type": "string"
          },
          "total": {
           "type": "integer"
          },
          "statusUrl": {
           "type": "string"
          },
          "errors": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Not authenticated."
     },
     "403": {
      "description": "Plan gate: one free try used, or the tool requires a higher tier."
     }
    }
   }
  },
  "/api/transcribe/{id}": {
   "get": {
    "tags": [
     "Extract"
    ],
    "summary": "Poll a transcription batch",
    "operationId": "transcribeStatus",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Per-file transcripts (text, language, duration, segments).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "done": {
           "type": "boolean"
          },
          "total": {
           "type": "integer"
          },
          "completed": {
           "type": "integer"
          },
          "failed": {
           "type": "integer"
          },
          "rows": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "403": {
      "description": "Not your batch."
     },
     "404": {
      "description": "Batch not found or expired (results live 1 hour)."
     }
    }
   }
  },
  "/api/file-search": {
   "post": {
    "tags": [
     "Extract"
    ],
    "summary": "Search a word/phrase across files",
    "description": "Up to 50 mixed files (text/CSV, PDF, DOCX, spreadsheets, photos via OCR). Required query, matched literally (never a regex); optional wholeWord/caseSensitive. A file that cannot be read returns an explicit error, never a false zero-matches. Poll GET /api/file-search/{id}.",
    "operationId": "fileSearch",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files",
         "query"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "One or more input files (field name: files)."
         },
         "query": {
          "type": "string"
         },
         "wholeWord": {
          "type": "string",
          "enum": [
           "true",
           "false"
          ]
         },
         "caseSensitive": {
          "type": "string",
          "enum": [
           "true",
           "false"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Search batch accepted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "batchId": {
           "type": "string"
          },
          "total": {
           "type": "integer"
          },
          "statusUrl": {
           "type": "string"
          },
          "errors": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Not authenticated."
     },
     "403": {
      "description": "Plan gate: one free try used, or the tool requires a higher tier."
     }
    }
   }
  },
  "/api/file-search/{id}": {
   "get": {
    "tags": [
     "Extract"
    ],
    "summary": "Poll a file-search batch",
    "operationId": "fileSearchStatus",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Per-file hits (location, snippet, offset, length). Check error before trusting count 0.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "done": {
           "type": "boolean"
          },
          "total": {
           "type": "integer"
          },
          "completed": {
           "type": "integer"
          },
          "failed": {
           "type": "integer"
          },
          "rows": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "403": {
      "description": "Not your batch."
     },
     "404": {
      "description": "Batch not found or expired (results live 1 hour)."
     }
    }
   }
  },
  "/api/pdf-to-markdown": {
   "post": {
    "tags": [
     "Extract"
    ],
    "summary": "PDF to structure-preserving Markdown (RAG)",
    "description": "Up to 25 PDFs into clean Markdown for retrieval pipelines: ATX headings, real tables, lists, page markers. Running headers/footers stripped by default (keepRunning=true to keep; pageBreaks=false to drop markers). Deterministic, no model. Poll GET /api/pdf-to-markdown/{id}.",
    "operationId": "pdfToMarkdown",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "multipart/form-data": {
       "schema": {
        "type": "object",
        "required": [
         "files"
        ],
        "properties": {
         "files": {
          "type": "array",
          "items": {
           "type": "string",
           "format": "binary"
          },
          "description": "One or more input files (field name: files)."
         },
         "keepRunning": {
          "type": "string",
          "enum": [
           "true",
           "false"
          ]
         },
         "pageBreaks": {
          "type": "string",
          "enum": [
           "true",
           "false"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Conversion batch accepted.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "batchId": {
           "type": "string"
          },
          "total": {
           "type": "integer"
          },
          "statusUrl": {
           "type": "string"
          },
          "errors": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "401": {
      "description": "Not authenticated."
     },
     "403": {
      "description": "Plan gate: one free try used, or the tool requires a higher tier."
     }
    }
   }
  },
  "/api/pdf-to-markdown/{id}": {
   "get": {
    "tags": [
     "Extract"
    ],
    "summary": "Poll a PDF to Markdown batch",
    "operationId": "pdfToMarkdownStatus",
    "security": [
     {
      "bearerAuth": []
     }
    ],
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Per-file markdown + stats (pages, headings, tables, words).",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "done": {
           "type": "boolean"
          },
          "total": {
           "type": "integer"
          },
          "completed": {
           "type": "integer"
          },
          "failed": {
           "type": "integer"
          },
          "rows": {
           "type": "array",
           "items": {
            "type": "object"
           }
          }
         }
        }
       }
      }
     },
     "403": {
      "description": "Not your batch."
     },
     "404": {
      "description": "Batch not found or expired (results live 1 hour)."
     }
    }
   }
  }
 },
 "components": {
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "bearerFormat": "ck_live_…",
    "description": "API key generated in My Account (requires a verified email; available on every plan including Free). Send as `Authorization: Bearer ck_live_…`."
   }
  },
  "responses": {
   "BadRequest": {
    "description": "Invalid request (missing/invalid `to`, unsupported format, or magic-byte mismatch).",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "Unauthorized": {
    "description": "Missing or invalid API key.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "OutOfCredits": {
    "description": "Monthly credits exhausted. Upgrade, or enable pay-as-you-go.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   },
   "RateLimited": {
    "description": "Per-minute rate limit exceeded — retry with backoff.",
    "content": {
     "application/json": {
      "schema": {
       "$ref": "#/components/schemas/Error"
      }
     }
    }
   }
  },
  "schemas": {
   "ConvertAccepted": {
    "type": "object",
    "properties": {
     "jobId": {
      "type": "string",
      "description": "Poll GET /api/v1/jobs/{jobId} with this id."
     },
     "creditCost": {
      "type": "integer",
      "description": "Credits charged for this job (1, 3 or 10)."
     },
     "creditsRemaining": {
      "type": "integer",
      "description": "Credits left this month after this job."
     }
    },
    "required": [
     "jobId"
    ]
   },
   "JobStatus": {
    "type": "object",
    "properties": {
     "status": {
      "type": "string",
      "enum": [
       "waiting",
       "active",
       "done",
       "failed"
      ],
      "description": "`done` includes a downloadUrl; `failed` includes an error."
     },
     "progress": {
      "type": "integer",
      "description": "0–100 while active."
     },
     "downloadUrl": {
      "type": "string",
      "description": "Present when status=done. Valid 15 minutes; reusable within that window."
     },
     "expiresIn": {
      "type": "integer",
      "description": "Seconds the downloadUrl stays valid (900)."
     },
     "toolReport": {
      "type": "object",
      "description": "Present when status=done for detection-based tools (op=blur-faces, redact, remove-watermark). `detected` is the number of faces/regions/watermark elements actually found — check for 0 to catch no-op results (e.g. a scanned PDF where nothing matched).",
      "properties": {
       "op": {
        "type": "string"
       },
       "detected": {
        "type": "integer"
       }
      }
     },
     "error": {
      "type": "string",
      "description": "Present when status=failed."
     }
    }
   },
   "CreditBalance": {
    "type": "object",
    "properties": {
     "plan": {
      "type": "string"
     },
     "creditsIncluded": {
      "type": "integer"
     },
     "creditsUsed": {
      "type": "integer"
     },
     "creditsRemaining": {
      "type": "integer"
     },
     "resetDate": {
      "type": "string",
      "format": "date-time",
      "description": "When the monthly allowance resets (1st of next month, UTC)."
     },
     "overageEnabled": {
      "type": "boolean"
     },
     "overageRatePer1000": {
      "type": "number",
      "description": "€ per 1,000 credits past the allowance."
     }
    }
   },
   "Error": {
    "type": "object",
    "properties": {
     "error": {
      "type": "string",
      "description": "Human-readable error message."
     }
    },
    "required": [
     "error"
    ]
   }
  }
 }
}