{"openapi":"3.1.0","info":{"title":"Lingivio Translate API","version":"1.0.0","description":"Translate documents without touching their layout. Three steps: upload a file, start a translation, retrieve the result.\n\nAccess requires a PAID plan; a free-plan key is refused with `plan.api_access_required`. Authenticate with `Authorization: Bearer llk_…` — keys are created in the app and shown once.\n\nEvery failure carries a stable machine-readable class from the shared error contract, with a `retryable` flag and the `credit_action` the ledger took. Rate limits are per key and reported on `X-RateLimit-Limit` / `X-RateLimit-Remaining`; a 429 carries `Retry-After`. A 503 from the platform cost control (`spend.ceiling_reached`, `spend.unavailable`, `ops.dispatch_halted`) carries `Retry-After` too: those classes are `retryable`, but a ceiling is a rolling 24h window, so honour the header rather than retrying immediately.\n\nIdempotency: send `Idempotency-Key` on any POST. Replaying the SAME key with the SAME body returns the original resource instead of creating a second one — safe to retry a request whose response you never saw, and it will not double-bill. Reusing a key with a DIFFERENT body is a mistake rather than a retry and is refused with `409 idempotency_conflict`. Keys are scoped to your account and remembered for 24 hours; after that the same key is a new request.\n\nDeprecation policy: `/v1` is stable. Additive changes (new fields, new optional parameters, new error classes) can ship at any time and clients must ignore fields they do not know. A breaking change gets a new version prefix; `/v1` then continues to be served for at least 12 months, and every response on a deprecated version carries a `Sunset` header with the removal date.","license":{"name":"Proprietary"}},"servers":[{"url":"https://staging-api.lingivio.com"}],"tags":[{"name":"Files","description":"Upload and manage input documents."},{"name":"Translations","description":"Start, follow and download translations."},{"name":"Batches","description":"Translate many documents as one reserved act."}],"paths":{"/v1/files":{"post":{"operationId":"uploadFile","summary":"Upload a file (single shot)","description":"Upload a document as `multipart/form-data` with the bytes in a `file` part. The response carries the file's VALIDATED content type (read from the bytes, not the extension), its detected source language and its real billing page count, so a job can be priced before it is started. Bodies above 25 MiB must use the resumable path (`POST /v1/files/uploads`). Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"An opaque, caller-chosen value (printable ASCII, ≤200 chars). A retry with the same key AND the same body resolves to the resource the first call created and is billed once; the same key with a different body is a 409.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"},"filename":{"type":"string","description":"overrides the part filename; the extension must name a supported format"}}}}}},"responses":{"201":{"description":"the stored file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFile"}}}},"400":{"description":"malformed request"},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"the idempotency key is in use for a different body"},"413":{"description":"over a size or storage limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"415":{"description":"unsupported format, or the bytes are not what the name claims","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"corrupt, encrypted or rejected content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"rate limited"}},"security":[{"bearerAuth":[]}]},"get":{"operationId":"listFiles","summary":"List uploaded files","description":"The caller's files that are still inside the 24h retention window, newest first. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"responses":{"200":{"description":"a page of files","content":{"application/json":{"schema":{"type":"object","required":["data","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicFile"}},"has_more":{"type":"boolean","description":"a further page exists beyond this one"}}}}}},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/files/uploads":{"post":{"operationId":"createUploadSession","summary":"Start a resumable upload","description":"Begin a multi-part upload for a large input. Upload each part with `PUT /v1/files/uploads/{uploadId}/parts/{number}`, then `POST .../complete`. Every part except the last must be exactly `min_part_bytes`. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["filename","bytes"],"properties":{"filename":{"type":"string"},"bytes":{"type":"integer","minimum":1,"description":"total size of the finished object"}}}}}},"responses":{"201":{"description":"the open session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicUploadSession"}}}},"400":{"description":"malformed request"},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"413":{"description":"over a size or storage limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"415":{"description":"unsupported format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"rate limited"}},"security":[{"bearerAuth":[]}]}},"/v1/files/uploads/{uploadId}/parts/{number}":{"put":{"operationId":"uploadPart","summary":"Upload one part","description":"Store one part of a resumable upload. The body is the raw bytes of the part. Re-uploading a part number REPLACES it, so an interrupted client resends only what is missing. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}},{"name":"number","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"responses":{"200":{"description":"the stored part","content":{"application/json":{"schema":{"type":"object","required":["part_number","etag","bytes"],"properties":{"part_number":{"type":"integer"},"etag":{"type":"string"},"bytes":{"type":"integer"}}}}}},"400":{"description":"bad part number, or an empty body"},"404":{"description":"no such session for this caller"},"409":{"description":"the session is closed or expired"}},"security":[{"bearerAuth":[]}]}},"/v1/files/uploads/{uploadId}/complete":{"post":{"operationId":"completeUpload","summary":"Complete a resumable upload","description":"Assemble the parts, run the content-based intake gate, count pages and detect the source language. Safe to retry: a completed session answers with the file it produced. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"201":{"description":"the stored file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFile"}}}},"404":{"description":"no such session for this caller"},"409":{"description":"no parts, or the part sizes are invalid"},"413":{"description":"over a size limit","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"415":{"description":"the bytes are not what the name claims","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"corrupt, encrypted or rejected content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/files/uploads/{uploadId}":{"delete":{"operationId":"abortUpload","summary":"Abandon a resumable upload","description":"Abort the R2 upload and release the reserved file id. Idempotent. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"uploadId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"aborted","content":{"application/json":{"schema":{"type":"object","required":["deleted","upload_id"],"properties":{"deleted":{"type":"boolean"},"upload_id":{"type":"string"}}}}}},"404":{"description":"no such session for this caller"}},"security":[{"bearerAuth":[]}]}},"/v1/files/{id}":{"get":{"operationId":"getFile","summary":"Read one file","description":"Fetch a file's metadata. A file belonging to another account answers 404 — never 403 — so an id cannot be used to probe other tenants. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the file","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicFile"}}}},"404":{"description":"no such file for this caller"}},"security":[{"bearerAuth":[]}]},"delete":{"operationId":"deleteFile","summary":"Delete a file early","description":"Remove the stored bytes before the 24h retention deadline. The row survives while a translation still references it, so job history stays intact. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Files"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"deleted","content":{"application/json":{"schema":{"type":"object","required":["deleted","id"],"properties":{"deleted":{"type":"boolean"},"id":{"type":"string"}}}}}},"404":{"description":"no such file for this caller"}},"security":[{"bearerAuth":[]}]}},"/v1/translations":{"post":{"operationId":"createTranslation","summary":"Translate an uploaded file","description":"Start an asynchronous translation of a file uploaded through `POST /v1/files`. Credits are reserved from the engine's own page count — never from anything in this request. Send `Idempotency-Key` to make a retry safe: a replay resolves to the job the first call created and bills once. Attach a `callback_url` to be told when the job finishes instead of polling: this API POSTs one JSON notification per terminal state (done, failed, canceled) to that https URL, signed `Layoutlock-Signature: ts=<epoch seconds>;h1=<hex HMAC-SHA256 of \"${ts}:${rawBody}\">` with your account's signing secret, and identified by a stable `Layoutlock-Event-Id` — verify the signature, reject a timestamp outside your own tolerance, and dedupe on the event id, because a retry repeats the same id and the same bytes. Delivery is retried on a bounded backoff until your endpoint answers 2xx. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Translations"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"An opaque, caller-chosen value (printable ASCII, ≤200 chars). A retry with the same key AND the same body resolves to the resource the first call created and is billed once; the same key with a different body is a 409.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslationRequest"}}}},"responses":{"201":{"description":"the queued translation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslation"}}}},"400":{"description":"malformed request, or an unsupported target language"},"402":{"description":"not enough page credits"},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"no such file or glossary for this caller"},"409":{"description":"the idempotency key is in use for a different body"},"413":{"description":"over the per-job page ceiling","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"the document could not be processed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"rate limited, or the concurrency limit is reached"},"503":{"description":"translation is paused, or the page count is unavailable"}},"security":[{"bearerAuth":[]}]},"get":{"operationId":"listTranslations","summary":"List translations","description":"The caller's translations, newest first, with status, page counts and error class. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Translations"],"responses":{"200":{"description":"a page of translations","content":{"application/json":{"schema":{"type":"object","required":["data","has_more"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicTranslation"}},"has_more":{"type":"boolean","description":"a further page exists beyond this one"}}}}}},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"security":[{"bearerAuth":[]}]}},"/v1/translations/batches":{"post":{"operationId":"createTranslationBatch","summary":"Translate several files as one batch","description":"Start translations for up to 20 already-uploaded files as ONE act. The credits for the whole batch are reserved atomically, so a batch can never half-run out of credits partway through; members are held `pending` and start as the plan's concurrency allows, so a batch queues behind the limit instead of bypassing it. A file that fails its own gate is reported in `rejected` and costs nothing — it never fails the batch. Send `Idempotency-Key` to make a retry safe: a replay resolves to the batch the first call created and bills once. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Batches"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"An opaque, caller-chosen value (printable ASCII, ≤200 chars). A retry with the same key AND the same body resolves to the resource the first call created and is billed once; the same key with a different body is a 409.","schema":{"type":"string","maxLength":200}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslationBatchRequest"}}}},"responses":{"201":{"description":"the created batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslationBatch"}}}},"400":{"description":"malformed request, or more than 20 files"},"402":{"description":"not enough page credits for the whole batch"},"403":{"description":"free plan, or an unconfirmed address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"the idempotency key is in use for a different body"},"422":{"description":"no file in the batch could be processed"},"429":{"description":"rate limited"},"503":{"description":"translation is paused"}},"security":[{"bearerAuth":[]}]}},"/v1/translations/batches/{id}/output":{"get":{"operationId":"downloadTranslationBatch","summary":"Download a batch as a zip","description":"Stream every FINISHED member of the batch as one zip archive. `X-Batch-Files` reports how many members the archive contains and `X-Batch-Total` how many the batch has, so a partial download is visible without unzipping it. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Batches"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the archive","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"no such batch for this caller"},"409":{"description":"no member of the batch has finished yet"}},"security":[{"bearerAuth":[]}]}},"/v1/translations/batches/{id}":{"get":{"operationId":"getTranslationBatch","summary":"Read one batch","description":"Per-file status, per-file error class and REAL per-file segment counts. There is deliberately no single batch status: `progress` carries the counts and each entry carries its own outcome, because one word over N documents cannot be honest. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Batches"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the batch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslationBatch"}}}},"404":{"description":"no such batch for this caller"}},"security":[{"bearerAuth":[]}]}},"/v1/translations/{id}/output":{"get":{"operationId":"downloadTranslation","summary":"Download the translated document","description":"Stream the finished document. `?variant=dual` serves the bilingual side-by-side PDF when the job produced one (`output.variants` says whether it did). Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Translations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"variant","in":"query","required":false,"description":"which output to download","schema":{"type":"string","enum":["mono","dual"],"default":"mono"}}],"responses":{"200":{"description":"the translated document","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"no such translation, or that variant was not produced"},"409":{"description":"the translation has not finished"}},"security":[{"bearerAuth":[]}]}},"/v1/translations/{id}/cancel":{"post":{"operationId":"cancelTranslation","summary":"Cancel a translation","description":"Stop a translation that has not finished. A queued job is cancelled immediately; a running one enters `cancel_requested` and holds its reserve until the engine confirms compute stopped. Retrying a cancel is not an error. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Translations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the updated translation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslation"}}}},"404":{"description":"no such translation for this caller"},"409":{"description":"the translation is already finished"}},"security":[{"bearerAuth":[]}]}},"/v1/translations/{id}":{"get":{"operationId":"getTranslation","summary":"Read one translation","description":"Status and REAL segment progress. `progress.done`/`progress.total` are the engine's own counts — there is no synthesised percentage. Requires a paid plan. A free-plan caller is refused with `plan.api_access_required` (403) — never a 404 and never an empty result.","tags":["Translations"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"the translation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTranslation"}}}},"404":{"description":"no such translation for this caller"}},"security":[{"bearerAuth":[]}]}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"An API key created in the app. Sent as `Authorization: Bearer llk_…`."}},"schemas":{"EngineError":{"title":"EngineError","description":"Every engine, pipeline and api-side failure maps to one of these classes, carrying a copy key, a retryable flag and a credit action. Unknown exceptions become engine.unexpected, and a raw traceback is never surfaced to users. The plan family is the ENTITLEMENT wall: plan.format_not_included, plan.file_too_large, plan.batch_not_included and plan.api_access_required all mean 'this is supported, but not on your plan' and each names the tier that unlocks it — never a generic unsupported/too-large answer, which would send a user away believing the product cannot do what it does. file.pagination_unavailable is the engine's honest refusal when a per-page charge has no page count to bill against (a .docx whose producer recorded no pagination); it is never substituted with a character estimate. Classes prefixed privacy, auth, email, plan, spend and ops are raised by the api and never originate in the engine; the auth, email and plan families always use a credit action of 'none', because they are refused before any reservation exists. The spend family is the platform cost control: spend.ceiling_reached and spend.unavailable are admission refusals that happen before any reservation, while ops.dispatch_halted can also refuse an already-reserved queued job at dispatch, which releases the reserve. credits.partial_unavailable is the refusal that answers 'translate as much of this file as I can afford' for a document that cannot honestly be split: a standalone image is two indivisible credits, a .docx is billed per displayed page and nothing in the file says which paragraph falls on which page, and a workbook's cell text is workbook-global so translating some worksheets would change others. It is raised before any reservation exists, so its credit action is 'none'. webhook.callback_url_invalid refuses a `callback_url` this server would later fetch: not https, carrying credentials, or naming a loopback, link-local (the cloud metadata endpoint), private or otherwise internal address. It is raised at REQUEST time, before a job or a reservation exists — refusing at delivery time would mean the request had already succeeded and the SSRF attempt had already been accepted — so its credit action is 'none' too.","type":"object","additionalProperties":false,"required":["class","message_key","retryable","credit_action"],"properties":{"class":{"type":"string","description":"stable machine-readable error class","enum":["file.too_large","file.too_many_pages","file.unsupported_format","file.type_mismatch","file.corrupt","file.password_protected","file.zip_bomb","storage.quota_exceeded","file.malware_detected","content.policy_violation","scan.unavailable","account.suspended","file.pagination_unavailable","pdf.no_text_layer","pdf.too_many_scanned_pages","pdf.encrypted","ocr.failed","llm.provider_down","llm.invalid_response","verify.invariant_failed","engine.timeout","engine.oom","engine.unexpected","privacy.export_rate_limited","privacy.deletion_confirmation_failed","privacy.consent_invalid","auth.email_unverified","auth.captcha_required","auth.captcha_failed","auth.verification_token_invalid","auth.verification_token_expired","auth.account_link_denied","email.send_failed","plan.api_access_required","plan.format_not_included","plan.file_too_large","plan.batch_not_included","credits.partial_unavailable","spend.ceiling_reached","spend.unavailable","ops.dispatch_halted","webhook.callback_url_invalid"]},"message_key":{"type":"string","description":"i18n message key resolved to user-facing copy at the edge per Accept-Language"},"retryable":{"type":"boolean","description":"whether re-enqueueing the job could succeed without user action"},"credit_action":{"type":"string","enum":["release","hold","none"],"description":"release: refund the reserve; hold: keep reserved (transient); none: no ledger change"},"detail":{"type":"string","description":"internal detail / traceback reference stored in job_events; never shown raw to users"}}},"ErrorEnvelope":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/EngineError"},"limit":{"type":"integer","description":"the numeric limit that was enforced, when one applies"}}},"PublicFile":{"title":"PublicFile","description":"An uploaded input file as the PUBLIC API describes it (step 1 of upload → translate → retrieve). Every field here is server-derived: `content_type` and `format` come from the intake gate's magic-byte inspection of the bytes you sent, NOT from the filename extension or a client-declared MIME type; `credits` is the engine's real billing count; `detected_source_language` is the engine's detection. A client can therefore price a translation before committing to one.","type":"object","additionalProperties":false,"required":["id","filename","bytes","content_type","format","credits","page_count","detected_source_language","detected_languages","status","created_at","expires_at"],"properties":{"id":{"type":"string","description":"file id, referenced by POST /v1/translations"},"filename":{"type":"string"},"bytes":{"type":"integer","minimum":0,"description":"the REAL stored object size, never the declared one"},"content_type":{"type":"string","description":"MIME type VALIDATED from the stored bytes (magic bytes / OPC part names), not inferred from the extension"},"format":{"type":"string","enum":["pdf","docx","xlsx","pptx","png","jpg","webp","txt","md","csv","html","srt"],"description":"canonical intake format the bytes were verified to be"},"credits":{"type":["integer","null"],"minimum":0,"description":"whole CREDITS the engine counted for this file under the per-format credit rule (a page, a slide, a worksheet block, or 3,000 characters; scans and images cost 2). This is what a translation of this file will reserve. null only when the count could not be produced - never a guess. `page_count` carries the same number under its old, misleading name."},"page_count":{"type":["integer","null"],"minimum":0,"description":"DEPRECATED ALIAS of `credits`, kept for /v1 stability — identical value, and it will keep being sent for the whole of v1. It is misnamed: since the 2026-08-08 credit restructure the number is CREDITS, not pages (a 4-page scanned PDF is 8, and a 12-worksheet spreadsheet whose sheets carry that much text can be 18). Read `credits`."},"physical_page_count":{"type":["integer","null"],"minimum":0,"description":"laid-out units this document contains: PDF pages, Word pages, PPTX slides. NULL — not 0 — for a format that has none at all (a spreadsheet, an image, plain text), so \"zero pages\" and \"pages do not apply to this format\" are distinguishable. Informational; the charge is `credits`."},"detected_source_language":{"type":["string","null"],"description":"dominant detected source language (BCP-47 subset), or null when detection produced no confident answer. Null is also the honest answer for any PDF with pages whose text only exists as pixels: accepting an upload deliberately classifies pages instead of running OCR on them, so those pages were never read. That applies to a PARTLY scanned document too — the born-digital pages of a hybrid PDF are not a sample of the whole, and reporting their language would be a confident answer about the minority of the file. POST /v1/detect performs the recognition and answers this on request."},"detected_languages":{"type":"object","additionalProperties":{"type":"number"},"description":"language code -> share of detected content (0..1); empty when detection was unavailable or produced no attributable span. Empty alongside a null `detected_source_language` is the normal answer for a PDF with any unread (scanned) page (see that field)."},"status":{"type":"string","enum":["ready"],"description":"a file is only ever returned once its bytes passed the intake gate"},"created_at":{"type":"integer","description":"epoch milliseconds"},"expires_at":{"type":["integer","null"],"description":"epoch milliseconds; the 24h retention deadline after which the bytes are deleted"}}},"PublicUploadSession":{"title":"PublicUploadSession","description":"A resumable (multi-part) upload in progress — the initiate → upload part → complete path of POST /v1/files, for inputs too large for one request. Backed by a real R2 multipart upload; the session and its parts live in D1 (truth), never in KV.","type":"object","additionalProperties":false,"required":["upload_id","file_id","filename","declared_bytes","min_part_bytes","max_parts","parts_received","created_at","expires_at"],"properties":{"upload_id":{"type":"string"},"file_id":{"type":"string","description":"the file id this session will produce on completion; reserved up front so a client can correlate"},"filename":{"type":"string"},"declared_bytes":{"type":"integer","minimum":1,"description":"size the client declared at initiate; the authoritative size is measured from the completed object"},"min_part_bytes":{"type":"integer","minimum":1,"description":"every part except the LAST must be exactly this size (an R2 multipart requirement)"},"max_parts":{"type":"integer","minimum":1},"parts_received":{"type":"array","description":"part numbers already stored, ascending — a resumed client uploads only what is missing","items":{"type":"integer","minimum":1}},"created_at":{"type":"integer"},"expires_at":{"type":"integer"}}},"PublicTranslation":{"title":"PublicTranslation","description":"A translation job as the PUBLIC API describes it (steps 2 and 3 of upload → translate → retrieve). `pending` is a member of a batch that is fully reserved but deliberately held off the queue until the plan's concurrency allows it to start — a batch queues behind the limit instead of bypassing it. `progress` carries REAL engine segment counts — never a synthesised percentage — and `error` is always one of the published EngineError classes, so a client can branch on `retryable` without parsing prose.","type":"object","additionalProperties":false,"required":["id","status","file_id","filename","target_language","source_languages","credits_estimate","credits_final","page_count_estimate","page_count_final","progress","output","error","created_at","finished_at"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["pending","queued","dispatched","running","cancel_requested","done","failed","needs_review","canceled"]},"file_id":{"type":"string"},"filename":{"type":["string","null"]},"target_language":{"type":"string"},"source_languages":{"type":"array","items":{"type":"string"},"description":"source hints recorded at creation; empty when the engine was left to detect"},"credits_estimate":{"type":["integer","null"],"minimum":0,"description":"whole CREDITS reserved for this job (counted by the engine, never supplied by the request)"},"page_count_estimate":{"type":["integer","null"],"minimum":0,"description":"DEPRECATED ALIAS of `credits_estimate`, kept for /v1 stability - identical value. It is misnamed: the number is CREDITS, not pages."},"credits_final":{"type":["integer","null"],"minimum":0,"description":"whole CREDITS actually charged once the job settled. Never more than `credits_estimate`: the settlement is capped at the reserve the caller was quoted."},"page_count_final":{"type":["integer","null"],"minimum":0,"description":"DEPRECATED ALIAS of `credits_final`, kept for /v1 stability - identical value. It is misnamed: the number is CREDITS, not pages."},"progress":{"type":"object","additionalProperties":false,"required":["done","total"],"properties":{"done":{"type":"integer","minimum":0},"total":{"type":"integer","minimum":0}}},"output":{"type":"object","additionalProperties":false,"required":["available","variants"],"properties":{"available":{"type":"boolean"},"variants":{"type":"array","items":{"type":"string","enum":["mono","dual"]},"description":"downloadable variants; `dual` is the bilingual side-by-side PDF"}}},"error":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["class","message_key","retryable","credit_action"],"properties":{"class":{"type":"string"},"message_key":{"type":"string"},"retryable":{"type":"boolean"},"credit_action":{"type":"string","enum":["release","hold","none"]}}}]},"created_at":{"type":"integer"},"finished_at":{"type":["integer","null"]}}},"PublicTranslationRequest":{"title":"PublicTranslationRequest","description":"Body of POST /v1/translations. References a file already uploaded through POST /v1/files and carries EVERY option the pipeline actually honours — and only those. A raster INPUT is always OCR-translated and has no toggle; pictures embedded inside a document do (`translate_images`), because that pass rewrites image bytes and adds a per-picture OCR charge, so it is opted into rather than defaulted.","type":"object","additionalProperties":false,"required":["file_id","target_language"],"properties":{"file_id":{"type":"string","minLength":1,"maxLength":200,"description":"id returned by POST /v1/files"},"target_language":{"type":"string","minLength":2,"maxLength":16,"description":"target language code (BCP-47 subset); must be supported for the file's format"},"source_language":{"type":["string","null"],"maxLength":16,"description":"override for the detected source language; null (default) lets the engine use its own detection"},"tone":{"type":["string","null"],"enum":["formal","informal",null],"description":"register the model should use (de Sie/du, es usted/tú); null = the model's default"},"side_by_side":{"type":"boolean","description":"also produce the bilingual side-by-side PDF variant (PDF inputs only; a no-op elsewhere)"},"translate_sheet_tabs":{"type":"boolean","description":"translate sheet tab / slide-note names, rewriting formula references atomically (xlsx)"},"mirror_layout_for_rtl":{"type":"boolean","description":"mirror layout direction when the target is a right-to-left script"},"translate_images":{"type":"boolean","description":"also translate text inside pictures embedded in the document. Off by default: it bills extra OCR credits and rewrites image bytes, so it is never applied to a request that did not ask for it. Those credits are included in the quoted count before any credit is reserved."},"bilingual_cells":{"type":"string","enum":["replace_all","replace_one","append"],"description":"how a single cell holding two source languages is handled (xlsx multilingual sheets)"},"keep_language":{"type":["string","null"],"maxLength":16,"description":"for bilingual_cells=replace_one: the source language left byte-identical"},"glossary_id":{"type":["string","null"],"maxLength":200,"description":"a glossary owned by the caller; terms are enforced verbatim"},"callback_url":{"type":["string","null"],"maxLength":2000,"description":"https URL this API POSTs one signed notification to when the translation reaches a terminal state (done, failed or canceled). The body carries the translation id, its status, the error class when it failed, the credits actually billed and whether an output exists — never document content, never segment text and never a download URL. Each delivery carries a stable event id (`Layoutlock-Event-Id`), and a retry of the same terminal event carries the same id and the same bytes, so a receiver can dedupe. Refused at REQUEST time with `webhook.callback_url_invalid` when the URL is not https, carries credentials, or names a loopback, link-local, private or otherwise internal address — a callback is a URL this server fetches, so the refusal happens before the job exists rather than at delivery time."}}},"PublicTranslationBatch":{"title":"PublicTranslationBatch","description":"A batch of translations. There is deliberately NO single batch status: a batch of twenty documents in which two failed is neither 'done' nor 'failed', and reducing it to one word is how a client ends up telling its user that everything worked. `progress` carries the counts, `files` carries each document's own outcome, and `rejected` carries the files that never became jobs at all — each with the error class that explains why. Credits follow the same rule: every admitted file holds its own reserve, a failed file releases its own reserve, and nothing about the batch is settled as a unit.","type":"object","additionalProperties":false,"required":["id","target_language","credits_estimate","page_count_estimate","progress","files","rejected","created_at"],"properties":{"id":{"type":"string"},"target_language":{"type":"string"},"credits_estimate":{"type":"integer","minimum":0,"description":"whole CREDITS reserved for the whole batch at admission, atomically. Counted by the engine per file, never supplied by the request."},"page_count_estimate":{"type":"integer","minimum":0,"description":"DEPRECATED ALIAS of `credits_estimate`, kept for /v1 stability - identical value. It is misnamed: the number is CREDITS, not pages."},"progress":{"type":"object","additionalProperties":false,"required":["total","done","failed","settled"],"properties":{"total":{"type":"integer","minimum":0,"description":"files that became jobs"},"done":{"type":"integer","minimum":0},"failed":{"type":"integer","minimum":0,"description":"members that ended failed, canceled or needs_review"},"settled":{"type":"boolean","description":"every member reached a terminal state — says the batch is FINISHED, never that it succeeded"}}},"files":{"type":"array","description":"one entry per admitted file, in submission order","items":{"type":"object","additionalProperties":false,"required":["file_id","translation_id","filename","status","credits_estimate","credits_final","page_count_estimate","page_count_final","progress","output","error"],"properties":{"file_id":{"type":"string"},"translation_id":{"type":"string","description":"the translation this file became; usable with every /v1/translations endpoint"},"filename":{"type":["string","null"]},"status":{"type":"string","enum":["pending","queued","dispatched","running","cancel_requested","done","failed","needs_review","canceled"]},"credits_estimate":{"type":["integer","null"],"minimum":0,"description":"whole CREDITS reserved for this job (counted by the engine, never supplied by the request)"},"page_count_estimate":{"type":["integer","null"],"minimum":0,"description":"DEPRECATED ALIAS of `credits_estimate`, kept for /v1 stability - identical value. It is misnamed: the number is CREDITS, not pages."},"credits_final":{"type":["integer","null"],"minimum":0,"description":"whole CREDITS actually charged once the job settled. Never more than `credits_estimate`: the settlement is capped at the reserve the caller was quoted."},"page_count_final":{"type":["integer","null"],"minimum":0,"description":"DEPRECATED ALIAS of `credits_final`, kept for /v1 stability - identical value. It is misnamed: the number is CREDITS, not pages."},"progress":{"type":"object","additionalProperties":false,"required":["done","total"],"properties":{"done":{"type":"integer","minimum":0},"total":{"type":"integer","minimum":0}},"description":"REAL engine segment counts for THIS file — never a synthesised percentage"},"output":{"type":"object","additionalProperties":false,"required":["available","variants"],"properties":{"available":{"type":"boolean"},"variants":{"type":"array","items":{"type":"string","enum":["mono","dual"]}}}},"error":{"anyOf":[{"type":"null"},{"type":"object","additionalProperties":false,"required":["class","message_key","retryable","credit_action"],"properties":{"class":{"type":"string"},"message_key":{"type":"string"},"retryable":{"type":"boolean"},"credit_action":{"type":"string","enum":["release","hold","none"]}}}]}}}},"rejected":{"type":"array","description":"files that never became jobs, because they failed their own admission (unsupported format, not uploaded, past retention, over the per-file credit ceiling, not included in the plan). They hold no reserve and cost nothing.","items":{"type":"object","additionalProperties":false,"required":["file_id","filename","reason"],"properties":{"file_id":{"type":"string"},"filename":{"type":["string","null"]},"reason":{"type":"string","description":"an error class from the shared error contract, or an api refusal code. Never prose."}}}},"created_at":{"type":"integer"}}},"PublicTranslationBatchRequest":{"title":"PublicTranslationBatchRequest","description":"Body of POST /v1/translations/batches. Several already-uploaded files translated as ONE commercial act: the credits for the whole batch are reserved atomically, so a batch can never half-run out of credits at file 14. Options are batch-wide — one target language, one glossary, one tone — because that is what a batch IS; a heterogeneous set of documents is a set of separate calls to POST /v1/translations. Every file goes through exactly the same gates as a single translation, and a file that fails its own gate is reported against that file rather than failing the batch.","type":"object","additionalProperties":false,"required":["file_ids","target_language"],"properties":{"file_ids":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","minLength":1,"maxLength":200},"description":"ids returned by POST /v1/files. Duplicates are collapsed — the same document is never reserved or delivered twice in one batch."},"target_language":{"type":"string","minLength":2,"maxLength":16,"description":"target language code (BCP-47 subset); must be supported for every file's format"},"source_language":{"type":["string","null"],"maxLength":16,"description":"override for the detected source language; null (default) lets the engine detect per file"},"tone":{"type":["string","null"],"enum":["formal","informal",null],"description":"register the model should use; null = the model's default"},"side_by_side":{"type":"boolean","description":"also produce the bilingual side-by-side PDF variant (PDF inputs only; a no-op elsewhere)"},"translate_sheet_tabs":{"type":"boolean","description":"translate sheet tab / slide-note names, rewriting formula references atomically (xlsx)"},"mirror_layout_for_rtl":{"type":"boolean","description":"mirror layout direction when the target is a right-to-left script"},"translate_images":{"type":"boolean","description":"also translate text inside pictures embedded in each document. Off by default: it bills extra OCR credits, and those credits are included in the quoted batch total before any credit is reserved."},"bilingual_cells":{"type":"string","enum":["replace_all","replace_one","append"],"description":"how a single cell holding two source languages is handled (xlsx multilingual sheets)"},"keep_language":{"type":["string","null"],"maxLength":16,"description":"for bilingual_cells=replace_one: the source language left byte-identical"},"glossary_id":{"type":["string","null"],"maxLength":200,"description":"a glossary owned by the caller; terms are enforced verbatim across the batch"}}},"TranslateSegmentsRequest":{"title":"TranslateSegmentsRequest","description":"Format-agnostic batch translation request. The web app, engines, and a future Chrome extension all call POST /v1/translate/segments with this shape. Formulas/numbers/codes must be filtered out (dnt=true) BEFORE they reach here — they never enter a prompt.","type":"object","additionalProperties":false,"required":["tgt","segments"],"properties":{"tgt":{"type":"string","maxLength":35,"description":"target language code"},"src_hint":{"type":"array","maxItems":8,"items":{"type":"string","maxLength":35},"description":"detected source language hints; may be empty"},"formality":{"type":"string","enum":["formal","informal"]},"glossary_id":{"type":["string","null"],"maxLength":128},"segments":{"type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string","maxLength":128,"description":"stable segment id, unique within the batch"},"text":{"type":"string","maxLength":32767,"description":"translatable text; may contain {0} {1} placeholders that MUST survive verbatim"},"context":{"type":"string","maxLength":512,"description":"optional hint for the model (e.g. 'spreadsheet cell', 'slide title')"},"lang":{"type":"string","maxLength":35,"description":"optional per-segment detected source language"}}}}}},"TranslateSegmentsResponse":{"title":"TranslateSegmentsResponse","description":"Response for POST /v1/translate/segments. One translation per input segment id; meta carries token/credit metering for the ledger.","type":"object","additionalProperties":false,"required":["translations","meta"],"properties":{"translations":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","text"],"properties":{"id":{"type":"string"},"text":{"type":"string"}}}},"meta":{"type":"object","additionalProperties":false,"required":["tokens_in","tokens_out","pages","segments","paragraphs","sentences","characters","model"],"properties":{"tokens_in":{"type":"integer","minimum":0},"tokens_out":{"type":"integer","minimum":0},"pages":{"type":"integer","minimum":0,"description":"whole billable CREDITS under the character rule, applied to the SOURCE text: max(1, ceil(visible source chars of the translatable segments / 3000)), or 0 when nothing was translatable. DNT/sacred segments are excluded and inline markup is stripped; cache hits ARE billed — the cache saves LLM cost, not the user's credit cost. Metering the source (rather than the delivered output, as this endpoint did until 2026-08-09) is what makes N characters cost the same here as in a .txt upload, and it is why the api reserves and settles the identical integer. The field is named `pages` and stays named `pages`: /v1 is a stable prefix and this endpoint is the extension-readiness surface a future client consumes unchanged."},"cached":{"type":"integer","minimum":0,"description":"segments served from cache"},"segments":{"type":"integer","minimum":0,"description":"successfully translated segments"},"paragraphs":{"type":"integer","minimum":0,"description":"translated paragraph/block count (best-effort analytics)"},"sentences":{"type":"integer","minimum":0,"description":"translated sentence count (best-effort analytics)"},"characters":{"type":"integer","minimum":0,"description":"visible translated characters, excluding inline markup"},"model":{"type":"string","minLength":1,"description":"provider/model id used for internal cost analytics"}}}}}}},"security":[{"bearerAuth":[]}],"x-error-classes":[{"class":"file.too_large","status":413,"retryable":false,"credit_action":"release","message_key":"error.file.too_large"},{"class":"file.too_many_pages","status":413,"retryable":false,"credit_action":"release","message_key":"error.file.too_many_pages"},{"class":"file.unsupported_format","status":415,"retryable":false,"credit_action":"release","message_key":"error.file.unsupported_format"},{"class":"file.type_mismatch","status":415,"retryable":false,"credit_action":"release","message_key":"error.file.type_mismatch"},{"class":"file.corrupt","status":422,"retryable":false,"credit_action":"release","message_key":"error.file.corrupt"},{"class":"file.password_protected","status":422,"retryable":false,"credit_action":"release","message_key":"error.file.password_protected"},{"class":"file.zip_bomb","status":422,"retryable":false,"credit_action":"release","message_key":"error.file.zip_bomb"},{"class":"storage.quota_exceeded","status":413,"retryable":false,"credit_action":"release","message_key":"error.storage.quota_exceeded"},{"class":"file.malware_detected","status":422,"retryable":false,"credit_action":"release","message_key":"error.file.malware_detected"},{"class":"content.policy_violation","status":422,"retryable":false,"credit_action":"release","message_key":"error.content.policy_violation"},{"class":"scan.unavailable","status":503,"retryable":true,"credit_action":"release","message_key":"error.scan.unavailable"},{"class":"account.suspended","status":403,"retryable":false,"credit_action":"release","message_key":"error.account.suspended"},{"class":"file.pagination_unavailable","status":422,"retryable":false,"credit_action":"release","message_key":"error.file.pagination_unavailable"},{"class":"pdf.no_text_layer","status":422,"retryable":false,"credit_action":"release","message_key":"error.pdf.no_text_layer"},{"class":"pdf.too_many_scanned_pages","status":413,"retryable":false,"credit_action":"release","message_key":"error.pdf.too_many_scanned_pages"},{"class":"pdf.encrypted","status":422,"retryable":false,"credit_action":"release","message_key":"error.pdf.encrypted"},{"class":"ocr.failed","status":422,"retryable":false,"credit_action":"release","message_key":"error.ocr.failed"},{"class":"llm.provider_down","status":503,"retryable":true,"credit_action":"hold","message_key":"error.llm.provider_down"},{"class":"llm.invalid_response","status":502,"retryable":false,"credit_action":"release","message_key":"error.llm.invalid_response"},{"class":"verify.invariant_failed","status":422,"retryable":false,"credit_action":"release","message_key":"error.verify.invariant_failed"},{"class":"engine.timeout","status":504,"retryable":true,"credit_action":"hold","message_key":"error.engine.timeout"},{"class":"engine.oom","status":500,"retryable":true,"credit_action":"hold","message_key":"error.engine.oom"},{"class":"engine.unexpected","status":500,"retryable":false,"credit_action":"hold","message_key":"error.engine.unexpected"},{"class":"privacy.export_rate_limited","status":429,"retryable":true,"credit_action":"none","message_key":"error.privacy.export_rate_limited"},{"class":"privacy.deletion_confirmation_failed","status":400,"retryable":false,"credit_action":"none","message_key":"error.privacy.deletion_confirmation_failed"},{"class":"privacy.consent_invalid","status":400,"retryable":false,"credit_action":"none","message_key":"error.privacy.consent_invalid"},{"class":"auth.email_unverified","status":403,"retryable":false,"credit_action":"none","message_key":"error.auth.email_unverified"},{"class":"auth.captcha_required","status":400,"retryable":true,"credit_action":"none","message_key":"error.auth.captcha_required"},{"class":"auth.captcha_failed","status":403,"retryable":true,"credit_action":"none","message_key":"error.auth.captcha_failed"},{"class":"auth.verification_token_invalid","status":400,"retryable":false,"credit_action":"none","message_key":"error.auth.verification_token_invalid"},{"class":"auth.verification_token_expired","status":410,"retryable":true,"credit_action":"none","message_key":"error.auth.verification_token_expired"},{"class":"auth.account_link_denied","status":403,"retryable":false,"credit_action":"none","message_key":"error.auth.account_link_denied"},{"class":"email.send_failed","status":502,"retryable":true,"credit_action":"none","message_key":"error.email.send_failed"},{"class":"plan.api_access_required","status":403,"retryable":false,"credit_action":"none","message_key":"error.plan.api_access_required"},{"class":"plan.format_not_included","status":403,"retryable":false,"credit_action":"none","message_key":"error.plan.format_not_included"},{"class":"plan.file_too_large","status":413,"retryable":false,"credit_action":"none","message_key":"error.plan.file_too_large"},{"class":"plan.batch_not_included","status":403,"retryable":false,"credit_action":"none","message_key":"error.plan.batch_not_included"},{"class":"credits.partial_unavailable","status":402,"retryable":false,"credit_action":"none","message_key":"error.credits.partial_unavailable"},{"class":"spend.ceiling_reached","status":503,"retryable":true,"credit_action":"none","message_key":"error.spend.ceiling_reached"},{"class":"spend.unavailable","status":503,"retryable":true,"credit_action":"none","message_key":"error.spend.unavailable"},{"class":"ops.dispatch_halted","status":503,"retryable":true,"credit_action":"release","message_key":"error.ops.dispatch_halted"},{"class":"webhook.callback_url_invalid","status":400,"retryable":false,"credit_action":"none","message_key":"error.webhook.callback_url_invalid"}]}