Document Translation
Start here: README · Related: Authentication · Conventions & errors · Text translation · Storage
The document endpoints translate PDF, DOCX and XLSX files in place — the original layout, pagination, tables and styling are preserved, and only the text is replaced. Submission is asynchronous by default: you upload a file, receive a job, poll it to completion, then optionally walk the document's segments, post human corrections, approve, and download the translated file. Use this page when you need the source file back in its original format; for plain-text or batch string translation use the text endpoints (POST /api/translations).
- Base URL (production):
https://trueidiom.com - Path prefix:
/api - API version:
?api-version=2026-09-01— optional; omitting it serves the oldest supported version.X-Api-Version: 2026-09-01is an accepted alias for the query parameter, which wins when both are sent, and a pin naming a version that has not been released is rejected with400naming the supported set. Full contract: Versioning
Authentication
These endpoints are published in the OpenAPI document served at /openapi.json. The generated document declares no securitySchemes and no global security requirement, so the interactive /docs page shows every endpoint as unauthenticated. They are not: auth is enforced server-side, and every endpoint on this page requires tenant credentials.
Send credentials in one of two ways:
| Method | Header(s) | Where you get it |
|---|---|---|
| Tenant API key | X-API-Key: <tenant_api_key> |
tenant_api_key in the POST /api/auth/signup/email response, or api_key from POST /api/tenants (admin-only). Returned exactly once — see Authentication. Optionally add X-Tenant-ID: <tenant_id> to pin the account when one key could match several. |
| Session access token | Authorization: Bearer <access_token> |
Issued by POST /api/auth/signup/email, POST /api/oauth2/token and POST /api/auth/mfa/verify — see the token endpoint. A Bearer value is also accepted as an API key if it does not resolve to a session. |
Every curl example on this page uses $TRUEIDIOM_API_KEY for a tenant API key. If you do not have one yet:
export TRUEIDIOM_API_KEY=$(curl -sS -X POST "https://trueidiom.com/api/auth/signup/email?api-version=2026-09-01" \
-H "Content-Type: application/json" \
-d '{"tenant_name":"Acme Legal","email":"you@example.com","password":"correct-horse-battery-staple"}' \
| jq -r .tenant_api_key)
Use the session access token instead when you want the reviewer identity on /review and /approve derived server-side rather than declared in the body.
Without usable credentials every endpoint below returns 401:
{ "detail": "missing or invalid tenant credentials" }
On deployments that run with a single global operator key and no tenant accounts, the same 401 reads {"detail": "missing or invalid API key"}.
Tenant scoping. Reads are scoped to the calling tenant. A document belonging to another tenant returns 404 document job not found — never 403 — so document ids are not confirmed to callers who may not see them. The global_administrator role does not widen this; it is a per-tenant admin role. Only the platform operator API key or a designated platform-operator session sees across tenants.
Reviewer identity. For /review and /approve, the server derives the reviewer from the authenticated session (email, falling back to user id) and that value wins over any reviewer field in the request body. A client-supplied reviewer is only honoured when no session can be resolved (API-key-only clients). See Reviewer identity.
Every response carries an X-Request-ID header. Send your own X-Request-ID to correlate with server logs.
The job model
A document upload creates a document translation job (TenantDocumentJob) that moves through these statuses:
status |
Meaning |
|---|---|
pending |
Model default; not observed on submitted jobs. |
queued |
Accepted and persisted; text extraction has not finished. |
extracted |
Text extraction finished; segment translation in progress. |
translated |
Terminal success. The quality report and the translated output file are both stored. |
failed |
Terminal failure. error holds the message and failure the structured detail. |
status only becomes translated after the translated output artifact is registered, so a poller that sees translated can download the translated file immediately.
POST /api/documents/translate
│
├── wait=true ── runs inline ──┐
│ │
└── default ──► queued ─────────┤
│ (poll GET /api/documents/{id} — there are no webhooks)
▼ │
extracted ────────┤
│ │
┌───────────┴─────────────┴───────────┐
▼ ▼
translated failed
│ (failure.code:
│ stage_timeout | transient_error
│ | unexpected_error | interrupted)
├──► GET .../download ← available now, and after every re-render
├──► GET .../segments?flagged=true
├──► POST .../review (repeatable; re-renders; status unchanged)
└──► POST .../approve (idempotent; stamps approved_at/approved_by;
feeds the golden translation memory)
Neither review nor approve changes status: a reviewed, approved document stays translated. Approval state lives in approved_at / approved_by.
Pipeline stages, in order, as they appear in failure.stage:
ingest → extract → map_translate → reconstruct
Review adds four more stage names (review_apply_corrections, review_validate, review_persist_layout, review_reconstruct), and a job interrupted by a service restart is marked failed with failure.stage = "worker".
What a job response contains
Every endpoint on this page that returns a job returns the same shape, whether the job is queued, translated or failed:
layoutis alwaysnull. The extracted layout is kept server-side and is not served. Reviewable content comes fromGET /api/documents/{document_id}/segments, which pages the same server-side walk the renderer and the approval sweep use.artifactslists the source document you uploaded and the translated output — the two kinds you can act on. The pipeline's own intermediate files are not listed, and entries carry no server-sidepath.quality_reportand each segment'squalitycarry review signals — scores,needs_review, terminology compliance, fast-path provenance — and not the details of how a segment was produced.billable_charsis the metering quantity for the job; see Tenants, usage & billing.
Platform-operator diagnostics (the platform operator API key or a designated platform-operator session) receive extended fields on these same routes; everything documented on this page is the tenant contract, which is also what /openapi.json publishes.
Layout preservation and in-place translation
A document is translated one unit at a time — a text region, or a single table cell — and each translation is written back into the document in place. What that preserves, per format:
- PDF — page count, page size, rotation and images are unchanged, and translated text is laid back into the same position and typography as the source. Where a translation runs longer than the source it replaces, the text is shrunk to fit its original box and, at the limit, truncated; neither happens silently — each is reported as a quality finding on the job (see Finding codes).
- DOCX / XLSX — styles, numbering, formulas and sheet structure survive untouched; only the text changes.
Because the unit of work is a region or a table cell, the reviewable segment ids you get back (region_id, cell_id) address exactly the units the translated file is written from. That is what lets POST /review put a corrected translation back into the one unit it belongs to. The re-render that follows is not partial: every review re-renders the entire output file and appends a new artifact.
Supported uploads
| Format | Extension | Content-Type for the file part |
Job source_format |
|---|---|---|---|
.pdf |
application/pdf (also application/x-pdf) |
pdf |
|
| Word | .docx |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
docx |
| Excel | .xlsx |
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
xlsx |
Detection rules:
- The file extension wins. HTTP clients routinely send
application/octet-streamfor Office files, soreport.docxwith a generic content type is still detected as DOCX. - If the name carries no known extension, the declared content type is used. When a file is matched on content type alone, the server appends the format's extension to the stored file name — uploading
contractasapplication/pdfyieldssource_file_name: "contract.pdf". - Anything else is rejected with
415, and the error names the accepted set.
Request bodies are capped at 30 MB (30,000,000 bytes). A body over the cap is rejected with 413 before it is read in full.
Language codes must come from the supported set — en, es, fr, de, it, pt-BR, pt-PT, nl, zh-Hans, zh-Hant, ja, ko, ar, ru, hi, tr, vi, th, id. Matching is case-insensitive on the exact code (EN → en); regional subtags and bare variant bases (en-US, fr-CA, zh, pt) are not accepted.
An unsupported code is rejected before any job is created. The pair is checked at the API boundary, the same check and the same body as
POST /api/translations:422, naming the codes it rejected and the whole supported set —{"detail": "unsupported language(s): 'en-US'. Supported: en, es, fr, …"}. Validating client-side still saves the round trip.
POST /api/documents/translate
Upload a source document and start a translation job.
Auth: tenant credentials required (401 without). Also gated by the deployment's translation switch (503 when off) and, when billing is enabled, by an active subscription (402). Layout document translation is never covered by the free-character trial — a subscription is required whenever billing is on.
Content type: multipart/form-data. The endpoint reads the form directly, which is why no request body appears in openapi.json; the fields it reads are below.
Form fields
| Name | Type | Required | Description |
|---|---|---|---|
source_document |
file | Yes | The source file. Missing or non-file → 400 source_document upload is required. Zero bytes → 400 source_document is empty. |
target_lang |
string | Yes | Target language code. Empty or absent → 400 target_lang is required. |
source_lang |
string | No | Source language code. Defaults to en. |
domain |
string | No | Domain hint (e.g. legal, medical) forwarded to the translation engine and echoed on request.domain. |
tone |
string | No | Register instruction applied to every segment's baseline translation (e.g. formal, informal, neutral). Same semantics as the TranslationRequest field. Blank or absent means no register instruction, not a default. |
gender |
string | No | Grammatical gender for agreement, applied to every segment's baseline translation. Governs agreement only — see the TranslationRequest field. |
text_type |
string | No | Accepted for request-shape symmetry with TranslationRequest and echoed on request.text_type. Plain (the default) is the only value that does anything here — see the note below. |
wait |
string | No | 1, true or yes (case-insensitive) runs the pipeline inline and returns 200 with the finished job. Anything else, or absent, queues the job and returns 202. |
text_type: "Html"does nothing on a document job. This endpoint handles PDF/DOCX/XLSX, and none of those formats carry their inline formatting as HTML tags — asking for markup to be reproduced verbatim would only put stray angle brackets in the output. The value is recorded on the job and otherwise ignored. To translate HTML, post the fragment or an.html/.htmupload toPOST /api/translations, which honorstext_typein full.
Headers
| Name | Required | Description |
|---|---|---|
Idempotency-Key |
No | Recorded on the job as request.idempotency_key. A resubmit with the same key returns the existing job instead of creating a second one. |
X-Request-ID |
No | Echoed back and bound to the server's log context. |
Response codes
| Status | When |
|---|---|
202 |
Queued. Body is the job in queued status. This is the default path. |
200 |
Returned for wait=true, and also for an idempotent resubmit whose job already reached translated or failed. |
400 |
Missing file, empty file, missing target_lang, or a text_type outside Plain/Html. |
401 |
Missing or invalid tenant credentials. |
402 |
Billing is enabled and the tenant has no active subscription. |
413 |
Request body exceeds the 30 MB upload cap. |
415 |
File type not in the supported set. |
422 |
source_lang or target_lang outside the supported set — unsupported language(s): …. Supported: …. No job is created. |
429 |
tenant usage limit exceeded — the tenant's monthly character quota is spent. Checked at submission, before the upload is read. |
503 |
Translation is switched off for the deployment. |
507 |
The platform storage cap is reached for this tenant. The detail names the bytes used and the cap, and points at deleting document jobs (DELETE /api/documents/{document_id}) or moving to your own storage (PUT /api/storage/binding). |
Example
curl -sS -X POST "https://trueidiom.com/api/documents/translate?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Idempotency-Key: contract-2026-07-26-001" \
-F "source_document=@contract.pdf;type=application/pdf" \
-F "source_lang=en" \
-F "target_lang=fr" \
-F "domain=legal"
HTTP/1.1 202 Accepted
{
"id": "b92fa968814843b08aea7b78f84d3894",
"tenant_id": "7fa2bff864961656",
"request": {
"source_lang": "en",
"target_lang": "fr",
"domain": "legal",
"idempotency_key": "contract-2026-07-26-001",
"metadata": {
"content_type": "application/pdf",
"size_bytes": 83421,
"idempotency_key": "contract-2026-07-26-001",
"tenant": { "id": "7fa2bff864961656", "name": "acme" }
},
"text_type": null,
"tone": null,
"gender": null
},
"status": "queued",
"source_file_name": "contract.pdf",
"source_format": "pdf",
"layout": null,
"quality_report": null,
"billable_chars": 0,
"review_history": [],
"artifacts": [
{
"id": "fef4070fce1241dbb28a3d9ffd910478",
"kind": "source_pdf",
"stage": "ingest",
"content_type": "application/pdf",
"checksum": "8f27627500903fb03ee21bac28b291bde1e451900a31e0763f496f2ec8e180ab",
"size_bytes": 83421,
"created_at": "2026-07-26T09:14:02.370869Z",
"metadata": { "source_file_name": "contract.pdf" }
}
],
"approved_at": null,
"approved_by": null,
"feed_manifest": { "fed_pairs": [], "skipped": {}, "golden_skipped_at_cap": 0, "last_feed_error": null },
"error": null,
"failure": null,
"created_at": "2026-07-26T09:14:02.368957Z",
"updated_at": "2026-07-26T09:14:02.370877Z"
}
Choosing sync vs async
wait=true holds the connection open until the document is finished and returns the completed job. Budgets are server-side: each stage gets 120 s and up to two automatic retries, and a busy service queues submissions rather than running them all at once. Prefer the default async path for anything larger than a couple of pages.
With wait=true, a stage failure surfaces as an error response rather than a job body — but the job is still persisted with status: "failed" and a populated failure object. Only a pipeline-typed failure produces the documented JSON error shape; an exception raised outside the pipeline's own stages can reach you as an untyped 500 with a body that is not JSON at all. Either way the persisted job record is the authoritative outcome, so read it rather than the response: resubmit with the same Idempotency-Key (which returns the existing job), or list GET /api/documents?status=failed.
Uploading a
.docx/.xlsxtoPOST /api/translationsasmultipart/form-datawith asource_documentpart routes into this same document pipeline and returns a document job in the same shape this endpoint returns. Poll and download it through the endpoints on this page.
GET /api/documents
List the calling tenant's document jobs, most recently updated first (updated_at descending).
Auth: tenant credentials required (401 without). Results are filtered to the caller's tenant.
layout is null here, as it is on every document response. Leaving it out is what keeps list responses small. quality_report, artifacts and review_history are included; page through reviewable content with GET /api/documents/{document_id}/segments.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | No | One of pending, queued, extracted, translated, failed. Anything else → 422. |
failure_code |
string | No | One of stage_timeout, transient_error, unexpected_error, interrupted. Enforced by pattern; other values → 422. |
failure_stage |
string | No | Exact stage name, min length 1 (e.g. extract, map_translate, reconstruct, worker). |
created_since |
string (date-time) | No | ISO 8601. Keeps jobs with created_at >= created_since. |
created_until |
string (date-time) | No | ISO 8601. Keeps jobs with created_at <= created_until. |
Example
curl -sS -G "https://trueidiom.com/api/documents?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
--data-urlencode "status=failed" \
--data-urlencode "failure_code=stage_timeout" \
--data-urlencode "created_since=2026-07-01T00:00:00Z"
[
{
"id": "b92fa968814843b08aea7b78f84d3894",
"tenant_id": "7fa2bff864961656",
"request": {
"source_lang": "en",
"target_lang": "fr",
"domain": "legal",
"idempotency_key": null,
"metadata": {
"content_type": "application/pdf",
"size_bytes": 83421,
"idempotency_key": null,
"tenant": { "id": "7fa2bff864961656", "name": "acme" }
},
"text_type": null,
"tone": null,
"gender": null
},
"status": "failed",
"source_file_name": "contract.pdf",
"source_format": "pdf",
"layout": null,
"quality_report": null,
"billable_chars": 0,
"review_history": [],
"artifacts": [
{
"id": "fef4070fce1241dbb28a3d9ffd910478",
"kind": "source_pdf",
"stage": "ingest",
"content_type": "application/pdf",
"checksum": "8f27627500903fb03ee21bac28b291bde1e451900a31e0763f496f2ec8e180ab",
"size_bytes": 83421,
"created_at": "2026-07-26T09:14:02.370869Z",
"metadata": { "source_file_name": "contract.pdf" }
}
],
"approved_at": null,
"approved_by": null,
"feed_manifest": { "fed_pairs": [], "skipped": {}, "golden_skipped_at_cap": 0, "last_feed_error": null },
"error": "TimeoutError",
"failure": {
"code": "stage_timeout",
"stage": "map_translate",
"message": "TimeoutError",
"error_type": "TimeoutError",
"retry_attempts": 2,
"transient": true,
"timeout": true,
"metadata": {}
},
"created_at": "2026-07-26T09:14:02.368957Z",
"updated_at": "2026-07-26T09:20:11.114003Z"
}
]
Errors: 401 (no credentials), 422 (invalid filter value).
GET /api/documents/summary
Aggregate job counts grouped by status and failure taxonomy.
Auth: tenant credentials required for the plain summary (401 without); the counts cover only the caller's tenant. The two breakdown flags aggregate across all tenants, so they are reserved for the platform operator API key or a designated platform-operator session — and which refusal you get depends on how you authenticated. A tenant API key is stopped at the admin gate with 401 admin credentials required and never reaches the flags. A signed-in admin who is not a platform operator — a global_administrator is exactly that — reaches them and gets 403 cross-tenant breakdowns require operator access; a signed-in non-admin is turned away one step earlier with 403 admin role required.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
string | No | Same enum as GET /api/documents. |
failure_code |
string | No | Same pattern as GET /api/documents. |
failure_stage |
string | No | Exact stage name, min length 1. |
created_since |
string (date-time) | No | ISO 8601 lower bound on created_at. |
created_until |
string (date-time) | No | ISO 8601 upper bound on created_at. |
include_tenant_breakdown |
boolean | No | Default false. Adds by_tenant. Operator only. |
include_stage_timing_breakdown |
boolean | No | Default false. Adds stage_timings_ms. Operator only. |
Example
curl -sS "https://trueidiom.com/api/documents/summary?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
{
"total": 38,
"failed": 2,
"by_status": { "translated": 35, "queued": 1, "failed": 2 },
"failures": {
"by_code": { "stage_timeout": 1, "unexpected_error": 1 },
"by_stage": { "map_translate": 1, "reconstruct": 1 }
}
}
With include_stage_timing_breakdown=true (operator credentials), the response gains:
{
"stage_timings_ms": {
"overall": {
"extract": { "count": 38, "avg_ms": 812.4, "p50_ms": 640.2, "p95_ms": 2104.9 },
"map_translate": { "count": 38, "avg_ms": 9310.7, "p50_ms": 7420.0, "p95_ms": 24110.5 },
"reconstruct": { "count": 37, "avg_ms": 1962.4, "p50_ms": 1801.0, "p95_ms": 3550.7 }
}
}
}
by_tenant (from include_tenant_breakdown=true) maps tenant_id — or the literal "unassigned" — to {"total": n, "failed": n}, and when both flags are set stage_timings_ms.by_tenant carries the same per-stage stats per tenant.
Errors: 401 (no credentials, or a tenant API key on the breakdown flags), 403 (a non-operator session on the breakdown flags), 422 (invalid filter value).
GET /api/documents/{document_id}
Fetch one job. This is the poll target: read status until it is translated or failed.
Auth: tenant credentials required (401 without). A job belonging to another tenant returns 404.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | Job id returned by POST /api/documents/translate. |
Example
curl -sS "https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
{
"id": "b92fa968814843b08aea7b78f84d3894",
"tenant_id": "7fa2bff864961656",
"request": {
"source_lang": "en",
"target_lang": "fr",
"domain": "legal",
"idempotency_key": "contract-2026-07-26-001",
"metadata": {
"content_type": "application/pdf",
"size_bytes": 83421,
"idempotency_key": "contract-2026-07-26-001",
"tenant": { "id": "7fa2bff864961656", "name": "acme" }
},
"text_type": null,
"tone": null,
"gender": null
},
"status": "translated",
"source_file_name": "contract.pdf",
"source_format": "pdf",
"layout": null,
"quality_report": {
"schema_version": "1.0",
"document_id": "e67fe11f03804045ab65cabe55473d82",
"source_lang": "en",
"target_lang": "fr",
"overall_score": 0.97,
"translated_units": 24,
"total_units": 24,
"findings": [],
"metadata": {
"page_count": 3,
"schema_version": "1.0",
"llm_quality": { "needs_review_units": 2, "noncompliant_units": 1 }
}
},
"billable_chars": 4820,
"review_history": [],
"artifacts": [
{
"id": "fef4070fce1241dbb28a3d9ffd910478",
"kind": "source_pdf",
"stage": "ingest",
"content_type": "application/pdf",
"checksum": "8f27627500903fb03ee21bac28b291bde1e451900a31e0763f496f2ec8e180ab",
"size_bytes": 83421,
"created_at": "2026-07-26T09:14:02.370869Z",
"metadata": { "source_file_name": "contract.pdf" }
},
{
"id": "2adee39df2994c288e2de190108f0040",
"kind": "translated_pdf",
"stage": "reconstruct",
"content_type": "application/pdf",
"checksum": "3515540984a6e48aff7821df05b50b97334c06c6b94d1a81b9b50a05bfd81670",
"size_bytes": 91234,
"created_at": "2026-07-26T09:14:12.403125Z",
"metadata": { "rendered_pages": 3 }
}
],
"approved_at": null,
"approved_by": null,
"feed_manifest": { "fed_pairs": [], "skipped": {}, "golden_skipped_at_cap": 0, "last_feed_error": null },
"error": null,
"failure": null,
"created_at": "2026-07-26T09:14:02.368957Z",
"updated_at": "2026-07-26T09:14:12.408112Z"
}
The two artifacts above are the whole list for a completed PDF job: the source you uploaded and the translated output. For reviewable text, use GET /api/documents/{document_id}/segments, which is paged.
Errors: 401 (no credentials), 404 (unknown id, or another tenant's job).
GET /api/documents/{document_id}/segments
Paged list of reviewable segments, walked server-side off the persisted layout. This walk is the single source of truth for what counts as a segment.
Auth: tenant credentials required (401 without). Cross-tenant → 404.
Semantics that matter:
indexis the segment's position in the full, unfiltered walk, so it stays stable across pages and across theflaggedfilter.totalis the size of the selected set (the filtered set whenflagged=true).flagged_totalalways counts flagged segments in the whole document, regardless offlagged, so you can render "N need attention" without a second request.- A job whose layout does not exist yet (
queued, orfailedbefore extraction) returnstotal: 0,flagged_total: 0,segments: []with yourlimit/offsetechoed back — not an error. - Table regions yield one segment per non-empty cell; every other region yields one segment from its source text. Empty-source regions and cells are dropped by the walk, which is exactly why
indexis not a positional index intolayout.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | Job id. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
flagged |
boolean | No | Default false. When true, only segments needing attention are returned (quality.needs_review is truthy or quality.terminology_compliant is exactly false). |
limit |
integer | No | Default 100. Minimum 1, maximum 500. Out of range → 422. |
offset |
integer | No | Default 0. Minimum 0. Negative → 422. An offset past the end returns an empty segments array with total unchanged. |
Response body (DocumentSegmentsResponse)
| Field | Type | Description |
|---|---|---|
total |
integer | Segments in the selected set. |
flagged_total |
integer | Flagged segments across the whole document. |
limit |
integer | Echo of the effective limit. |
offset |
integer | Echo of the effective offset. |
segments |
array of DocumentSegment |
The page of segments. |
Each DocumentSegment:
| Field | Type | Description |
|---|---|---|
index |
integer | Position in the full unfiltered walk. |
page |
integer | 1-based page number. |
target |
string | region or table_cell. |
region_id |
string | 32-character hex region id — pass this back on a correction. |
cell_id |
string | null | "{table_id}:{row}:{column}" for table cells — a 32-character hex table id, a 0-based row and a 0-based column. null for regions. Echo it back verbatim on a correction; do not construct it yourself. |
source |
string | Source text of the unit. |
translated |
string | null | Current translation; null if the unit was not translated. |
quality |
object | The segment's review signals; {} when none were recorded. See below. |
quality carries the review signals for the segment and nothing else. The complete set of keys it can contain:
| Key | Type | Meaning |
|---|---|---|
score |
number | Quality score for this segment, 0–100. |
needs_review |
boolean | The segment was flagged for a human reviewer. Cleared when a reviewer corrects it. |
below_threshold |
boolean | The score fell under the configured quality threshold. |
terminology_compliant |
boolean | Whether the translation used the tenant's enforced glossary terms. |
missing_terms |
object | Glossary terms expected but not found, as {term: count}. |
fast_path |
string | null | "tm_exact" or "glossary_exact" when the text was published verbatim from translation memory or the glossary — such a segment carries needs_review: false. |
corrected |
boolean | true once a human review edited this segment. |
Keys are present only when the service recorded them: score and needs_review appear when per-segment quality scoring is enabled on the deployment, and fast_path appears whenever the text came straight from translation memory or the glossary. Scoring internals — traces, token counts, and what produced a given segment — are not part of this object.
Example
curl -sS -G "https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894/segments?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
--data-urlencode "flagged=true" \
--data-urlencode "limit=50" \
--data-urlencode "offset=0"
{
"total": 2,
"flagged_total": 2,
"limit": 50,
"offset": 0,
"segments": [
{
"index": 0,
"page": 1,
"target": "region",
"region_id": "577ab4ee0485426fac76b74e22f4fdbc",
"cell_id": null,
"source": "Termination for cause",
"translated": "Résiliation pour cause",
"quality": {
"score": 74.0,
"below_threshold": true,
"terminology_compliant": true,
"missing_terms": {},
"needs_review": true
}
},
{
"index": 7,
"page": 2,
"target": "table_cell",
"region_id": "9c31e0af26b4471da58f7d0c1b6e2453",
"cell_id": "4bde2f9c1a7048e6b39d5c81f0a2e743:0:3",
"source": "Net 30",
"translated": "Net 30",
"quality": { "needs_review": true, "terminology_compliant": false }
}
]
}
Errors: 401 (no credentials), 404 (unknown id, or another tenant's job), 422 (limit/offset out of bounds).
GET /api/documents/{document_id}/artifacts
List the job's document files: the source you uploaded and the translated output.
Auth: tenant credentials required (401 without). Cross-tenant → 404.
This is a metadata listing (TenantDocumentArtifact[]) — there is no per-artifact fetch endpoint. Retrieve the bytes with GET /api/documents/{document_id}/download, read the quality report inline from the job object, and page reviewable content with GET /api/documents/{document_id}/segments. The pipeline's own intermediate files are not listed (see What a job response contains).
Artifact kinds
kind |
Produced at stage |
content_type |
|---|---|---|
source_pdf |
ingest |
application/pdf |
source_document |
ingest |
the DOCX or XLSX content type (non-PDF sources) |
translated_pdf |
reconstruct, and again at review |
application/pdf |
translated_document |
reconstruct, and again at review |
the DOCX or XLSX content type |
Artifacts are append-only. Each review re-render appends a new translated_pdf / translated_document entry with a fresh checksum; the last entry of that kind is the one whose checksum matches the stored bytes, and the one GET .../download serves.
Artifact fields (TenantDocumentArtifact)
| Field | Type | Description |
|---|---|---|
id |
string | Artifact id. |
kind |
string | One of the kinds above. |
stage |
string | Pipeline stage that produced it. |
content_type |
string | MIME type of the stored bytes. |
checksum |
string | SHA-256 hex digest, verified on download. |
size_bytes |
integer | Size of the stored bytes. |
created_at |
string (date-time) | Creation timestamp. |
metadata |
object | Kind-specific extras about your document: source_file_name (ingest), rendered_pages (PDF render), applied_segments (DOCX/XLSX render), and applied_corrections / rejected_corrections / impacted_pages (review re-renders). |
Example
curl -sS "https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894/artifacts?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
{
"document_id": "b92fa968814843b08aea7b78f84d3894",
"artifacts": [
{
"id": "fef4070fce1241dbb28a3d9ffd910478",
"kind": "source_pdf",
"stage": "ingest",
"content_type": "application/pdf",
"checksum": "8f27627500903fb03ee21bac28b291bde1e451900a31e0763f496f2ec8e180ab",
"size_bytes": 83421,
"created_at": "2026-07-26T09:14:02.370869Z",
"metadata": { "source_file_name": "contract.pdf" }
},
{
"id": "2adee39df2994c288e2de190108f0040",
"kind": "translated_pdf",
"stage": "reconstruct",
"content_type": "application/pdf",
"checksum": "3515540984a6e48aff7821df05b50b97334c06c6b94d1a81b9b50a05bfd81670",
"size_bytes": 91234,
"created_at": "2026-07-26T09:14:12.403125Z",
"metadata": { "rendered_pages": 3 }
}
]
}
Errors: 401 (no credentials), 404 (unknown id, or another tenant's job).
GET /api/documents/{document_id}/download
Stream the translated file.
Auth: tenant credentials required (401 without). Cross-tenant → 404.
Selection rules:
- If the job is still
queued, respond409— serving what exists would hand back an untranslated document. Keep polling. The409is keyed onqueuedonly; a job in any other status falls through to artifact selection. - Otherwise take the most recent
translated_pdf/translated_documentartifact. - If there is none (e.g. the job failed after ingest, or is mid-pipeline in
extracted), fall back to the most recentsource_pdf/source_document. Checkstatus == "translated"before treating the bytes as a translation. - If neither exists, respond
404— this is what apendingjob (which has no artifacts) returns, not409.
Only call this once status is translated if you want the translated file. Any earlier status either blocks you (409) or hands you the source bytes with a 200.
The stored SHA-256 is re-verified against the bytes before they are returned; a mismatch fails the request with 500 document artifact failed integrity check rather than serving suspect content.
Response headers:
| Header | Value |
|---|---|
Content-Type |
The selected artifact's content_type — application/pdf, or the DOCX/XLSX content type for Office sources. |
Content-Disposition |
attachment; filename="<source_file_name>" — note this is the source file name, not a translated-suffixed name. Rename client-side if you need contract.fr.pdf. |
Example
curl -sS -o contract.fr.pdf -D - \
"https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894/download?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
HTTP/1.1 200 OK
content-type: application/pdf
content-disposition: attachment; filename="contract.pdf"
content-length: 91234
x-request-id: c2ac6a3b91ad4608ad744a56fd74d448
Errors: 401 (no credentials), 404 (unknown id, another tenant's job, no downloadable artifact, or the stored bytes are missing), 409 (job still queued), 500 (checksum mismatch).
POST /api/documents/{document_id}/review
Apply human corrections to individual segments, then re-validate, re-render and persist the document.
Auth: tenant credentials required (401 without). Cross-tenant → 404, checked before anything is mutated. The reviewer identity is derived server-side; see Reviewer identity.
What one call does, in order: apply each correction to the layout → re-run the document validator and replace quality_report → persist the corrected layout server-side → re-render the entire output file — not only the pages corrections touched — and append a new translated artifact → feed the applied corrections into the tenant's golden translation memory (best-effort) → append a DocumentReviewRecord to review_history.
Review is repeatable. A review that succeeds does not approve the document and does not change status — the job stays translated. A review that raises once it is under way is the exception: the job is marked failed, with the stage it died in recorded as a review_* value in failure.stage. The typed refusals below (400, 404) are checked before any stage begins and leave the job exactly as it was.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | Job id. |
Request body (DocumentReviewRequest, application/json, required)
| Name | Type | Required | Description |
|---|---|---|---|
corrections |
array of DocumentReviewCorrection |
No | Defaults to []. An empty array is accepted and records a review with zero applied corrections. |
notes |
string | null | No | Free-text note stored on the review record. |
reviewer |
string | null | No | Only used when no session identity can be derived. A signed-in caller's identity always wins. |
DocumentReviewCorrection:
| Name | Type | Required | Description |
|---|---|---|---|
region_id |
string | Yes | From the segment you are correcting. |
translated_text |
string | Yes | The reviewer-typed replacement translation. |
target |
string | No | region (default) or table_cell. |
cell_id |
string | null | No | Required when target is table_cell; use the segment's cell_id verbatim. |
note |
string | null | No | Per-correction note. |
Conflicts
A correction that cannot be located is rejected, not fatal — the rest of the batch still applies. Each rejection appears in conflicts with a reason, and the distinct reasons are collected into conflict_codes:
reason |
Meaning |
|---|---|
region_not_found |
No region with that region_id on any page. |
cell_id_missing |
target: "table_cell" with no cell_id. |
cell_id_not_found |
The region exists but holds no cell with that cell_id. |
Response body (TenantDocumentReviewResponse)
| Field | Type | Description |
|---|---|---|
job |
TenantDocumentJob |
The updated job, including the new review_history entry, the new output artifact and the refreshed quality_report. |
review_result |
DocumentReviewSummary |
Compact summary of this call. |
DocumentReviewSummary:
| Field | Type | Description |
|---|---|---|
reviewer |
string | The identity the review was actually recorded against. |
applied_corrections |
integer | Corrections written into the layout. |
rejected_corrections |
integer | Corrections that could not be located. |
impacted_pages |
array of integer | Sorted page numbers touched. |
conflict_codes |
array of string | Sorted distinct rejection reasons. |
conflicts |
array of object | One entry per rejection: target, region_id, reason, plus cell_id on table-cell rejections. |
fed_pairs |
integer | Corrected pairs newly entered into the golden translation memory. Deduplicated within the batch and against the job's feed manifest, so an unchanged re-review feeds 0. |
feed_error |
string | null | Set when the memory feed failed. The feed is best-effort and never fails the review. |
A pair counted in fed_pairs is one this document offered to the memory, which is not always one the memory kept: when the tenant is at its translation-memory cap for the language pair, a pair carrying a new source is skipped rather than stored. That is not a feed_error — the running count is on the job as feed_manifest.golden_skipped_at_cap.
Example
curl -sS -X POST \
"https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894/review?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"reviewer": "reviewer@acme.example",
"notes": "Legal review, pass 1.",
"corrections": [
{
"target": "region",
"region_id": "577ab4ee0485426fac76b74e22f4fdbc",
"translated_text": "Résiliation pour motif valable",
"note": "House style."
},
{
"target": "table_cell",
"region_id": "0000000000000000000000000000dead",
"cell_id": "4bde2f9c1a7048e6b39d5c81f0a2e743:0:3",
"translated_text": "Paiement à 30 jours"
}
]
}'
The second correction carries a deliberately bogus region_id so the response below shows what a rejection looks like. Ids returned by GET .../segments always resolve — echo region_id and cell_id back exactly as you received them and both corrections apply.
reviewer is supplied here because the call authenticates with an API key and has no session to derive an identity from. A caller sending an Authorization: Bearer session token can omit it — and if it is sent anyway, the session identity is what gets recorded.
The job field carries the whole updated job, in the same shape every document endpoint returns; the abbreviated form below shows only the parts this call changed.
{
"job": {
"id": "b92fa968814843b08aea7b78f84d3894",
"status": "translated",
"review_history": [
{
"reviewer": "reviewer@acme.example",
"notes": "Legal review, pass 1.",
"applied_corrections": 1,
"rejected_corrections": 1,
"impacted_pages": [1],
"conflicts": [
{
"target": "table_cell",
"region_id": "0000000000000000000000000000dead",
"cell_id": "4bde2f9c1a7048e6b39d5c81f0a2e743:0:3",
"reason": "region_not_found"
}
],
"fed_pairs": 1,
"feed_error": null,
"created_at": "2026-07-26T09:28:09.071039Z"
}
]
},
"review_result": {
"reviewer": "reviewer@acme.example",
"applied_corrections": 1,
"rejected_corrections": 1,
"impacted_pages": [1],
"conflict_codes": ["region_not_found"],
"conflicts": [
{
"target": "table_cell",
"region_id": "0000000000000000000000000000dead",
"cell_id": "4bde2f9c1a7048e6b39d5c81f0a2e743:0:3",
"reason": "region_not_found"
}
],
"fed_pairs": 1,
"feed_error": null
}
}
Errors:
| Status | Body / cause |
|---|---|
400 |
reviewer could not be determined: sign in or supply a reviewer. |
400 |
{"detail": "document layout is unavailable", "error_code": "validation_error"} — the job has no layout yet. |
401 |
Missing or invalid tenant credentials. |
404 |
{"detail": "document job <id> not found", "error_code": "not_found"}, or another tenant's job. |
422 |
Malformed body (e.g. a correction missing region_id or translated_text). |
How this differs from text-pipeline review
Document review (POST /api/documents/{document_id}/review) |
Text review (POST /api/translations/{job_id}/review) |
|
|---|---|---|
| Unit of correction | Many segments per call, addressed by region_id / cell_id |
One final_translation for the whole job |
| Body | DocumentReviewRequest — corrections[], notes, optional reviewer |
ReviewSubmission — action (save/approve/modify/reject), final_translation, notes, required reviewer |
| Reviewer identity | reviewer is optional: a resolvable session identity is used, otherwise the body value |
reviewer is required in the body |
| Side effects | Re-validates, re-renders the output file, appends new artifacts | Transitions the text job's status |
| Approval | Separate call — POST /api/documents/{document_id}/approve |
Expressed as action: "approve" in the same call |
| Repeatable | Yes; each call appends to review_history |
Status-transition driven |
POST /api/documents/{document_id}/approve
Stamp the document approved and feed every eligible segment into the tenant's golden translation memory.
Auth: tenant credentials required (401 without). Cross-tenant → 404, checked before anything is mutated, so a cross-tenant caller can never feed another tenant's content into the golden store. Reviewer identity is derived server-side.
Approval is the countable learning event. Review feeds only the human-corrected pairs; approve sweeps the remaining clean segments and records the golden audit.
Eligibility. A segment feeds the golden store unless it is excluded, and every exclusion is tallied in skipped:
skipped key |
Excluded because |
|---|---|
empty |
Source or target is blank, or the two are identical after normalisation. |
fast_path |
The text came verbatim from translation memory or the glossary (quality.fast_path), so it teaches nothing new. |
needs_review |
Still flagged for review and never corrected by a human. |
duplicates |
Another segment with the same normalised source already won; a corrected pair beats an uncorrected one. |
Human-corrected segments (quality.corrected) always feed, and are ordered first, regardless of fast_path / needs_review.
Idempotent. A second approve returns already_approved: true, fed_pairs: 0 and replays the recorded skipped tally. approval_result.reviewer is then the original approver, not the current caller.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | Job id. |
Request body (DocumentApprovalRequest, application/json, required)
The body is required; send {} when you have nothing to add.
| Name | Type | Required | Description |
|---|---|---|---|
notes |
string | null | No | Free-text approval note. |
reviewer |
string | null | No | Only used when no session identity can be derived. |
Response body (TenantDocumentApprovalResponse)
| Field | Type | Description |
|---|---|---|
job |
TenantDocumentJob |
The job with approved_at, approved_by and the updated feed_manifest. |
approval_result.approved |
boolean | Always true on a 200. |
approval_result.already_approved |
boolean | true on an idempotent replay. |
approval_result.reviewer |
string | null | job.approved_by — who the approval is recorded against. |
approval_result.fed_pairs |
integer | Pairs newly entering the feed manifest on this call. Pairs already fed during review are not double-counted. |
approval_result.skipped |
object | {"fast_path": n, "needs_review": n, "duplicates": n, "empty": n}. |
approval_result.feed_error |
string | null | Set when the memory/audit feed failed. Best-effort: approval still stamps the job. |
A full translation memory does not fail an approval. When the tenant is at its translation-memory cap for the job's language pair, approved pairs carrying a source the memory does not already hold are skipped and not stored, and counted cumulatively in job.feed_manifest.golden_skipped_at_cap. Corrections are still admitted at the cap, so reviewer edits keep teaching the memory. A non-zero golden_skipped_at_cap means later identical requests will not get a free exact hit from those pairs; free budget for the pair with DELETE /api/tm/{set_id} on an imported set (see Terminology & translation memory).
Example
curl -sS -X POST \
"https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894/approve?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{"reviewer": "reviewer@acme.example", "notes": "Approved for delivery."}'
The job below is abbreviated to the fields approval changes, and feed_manifest.fed_pairs to its first entry — the manifest holds one entry per pair ever fed for this document, so after an approve that fed 22 it carries many more than the one shown.
{
"job": {
"id": "b92fa968814843b08aea7b78f84d3894",
"status": "translated",
"approved_at": "2026-07-26T09:31:44.512004Z",
"approved_by": "reviewer@acme.example",
"feed_manifest": {
"fed_pairs": [
{
"source_text": "Termination for cause",
"target_text": "Résiliation pour motif valable",
"source_hash": "1a3f9c0e2b7d4854af1b6c9d0e5f2a73c8b41d6e9f0a2b3c4d5e6f708192a3b4",
"corrected": true,
"stage": "review",
"created_at": "2026-07-26T09:28:09.070112Z"
}
…
],
"skipped": { "fast_path": 1, "needs_review": 1, "duplicates": 0, "empty": 0 },
"golden_skipped_at_cap": 0,
"last_feed_error": null
}
},
"approval_result": {
"approved": true,
"already_approved": false,
"reviewer": "reviewer@acme.example",
"fed_pairs": 22,
"skipped": { "fast_path": 1, "needs_review": 1, "duplicates": 0, "empty": 0 },
"feed_error": null
}
}
Errors:
| Status | Body / cause |
|---|---|
400 |
reviewer could not be determined: sign in or supply a reviewer. |
400 |
{"detail": "document job <id> is not in a completed state (status=extracted)", "error_code": "validation_error"} — only a translated job can be approved. |
400 |
{"detail": "document layout is unavailable", "error_code": "validation_error"}. |
401 |
Missing or invalid tenant credentials. |
404 |
{"detail": "document job <id> not found", "error_code": "not_found"}, or another tenant's job. |
422 |
Malformed body. |
DELETE /api/documents/{document_id}
Hard-delete one document job and everything it produced (self-service content deletion).
Auth: admin only, and — the platform operator API key aside — it has to be a session: an Authorization: Bearer <access_token> for a user holding the global_administrator role in the document's own tenant, as in the example below. A tenant API key never satisfies this gate. It is refused with 401 admin credentials required — not 403, and never success — however admin the tenant it belongs to. A session that authenticates but does not hold the role gets 403 admin role required. An admin of another tenant gets 404 (deletion never confirms a foreign document id exists). Only the operator key crosses tenants.
The delete cascades. Removed along with the job record:
- every artifact — every file stored for the job (source, intermediate data, quality report, translated output) and, for a Bring-Your-Own-Storage tenant, the blobs in the tenant's own storage account. A BYOS deletion failure is logged and never blocks the platform-side delete; the count reflects only what was actually removed.
- all segments — they live inside the job record and the files deleted with it.
- golden audit record(s) — the
document_approvalrecord the approval wrote into the golden system-of-record (platform store, plus the tenant's BYOS golden store when bound). - TM pairs — exactly the pairs this document fed into the tenant's
golden-approved-<src>-<tgt>set (from the job's feed manifest, matched by normalized source hash). Sibling pairs and client TMX imports are never touched. - learning rows — only rows provably derived from this document. A row without that linkage is left in place rather than risk deleting another tenant's data.
A job still in flight is cancelled before anything is removed, so a running translation cannot write artifacts back after the delete.
Deliberately retained: usage/billing records and the auth audit log survive deletion (legal/tax retention — see the privacy policy).
Deletion is permanent — there is no undo. Afterwards GET /api/documents/{document_id}, /artifacts, /segments, and /download all return 404.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
document_id |
string | Yes | Document job to delete. |
Response
200 OK. Counts report what was actually removed, not what was expected:
{
"deleted": true,
"document_id": "b92fa968814843b08aea7b78f84d3894",
"artifacts_removed": 5,
"segments_removed": 24,
"golden_records_deleted": 1,
"tm_pairs_removed": 22,
"learning_rows_removed": 2
}
Example
curl -X DELETE \
"https://trueidiom.com/api/documents/b92fa968814843b08aea7b78f84d3894?api-version=2026-09-01" \
-H "Authorization: Bearer $ACCESS_TOKEN"
Errors:
| Status | Body / cause |
|---|---|
401 |
admin credentials required — no usable credentials, or a tenant API key, which never passes this gate. |
403 |
admin role required — a session authenticated, but not holding global_administrator. |
404 |
document job not found — unknown id, another tenant's job, or an already-deleted job (double delete). |
Schemas
TenantDocumentJob
The body of POST /api/documents/translate, every entry of GET /api/documents, GET /api/documents/{document_id}, and the job field of the review and approval responses. This is the shape /openapi.json publishes; see What a job response contains for what it deliberately does not carry.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
id |
string | No | generated | 32-character hex job id. |
tenant_id |
string | null | No | null |
Owning tenant. null on a single-tenant deployment with no tenant accounts. |
request |
DocumentTranslationRequest |
Yes | — | The submitted request. See below. |
status |
DocumentJobStatus |
No | pending |
pending, queued, extracted, translated, failed. |
source_file_name |
string | Yes | — | Stored upload name. Also the Content-Disposition filename on download. |
source_format |
string | No | "pdf" |
pdf, docx, or xlsx. |
layout |
null |
No | null |
Always null. The key is kept so job.layout reads keep parsing; reviewable content comes from GET .../segments. |
quality_report |
DocumentQualityReport | null |
No | null |
Populated when the document is validated, and refreshed by every review. |
billable_chars |
integer | No | 0 |
Billable source characters for the job — the metering quantity; see Tenants, usage & billing. |
review_history |
DocumentReviewRecord[] |
No | [] |
One entry appended per POST .../review. |
artifacts |
TenantDocumentArtifact[] |
No | [] |
Append-only; source and translated documents only. See Artifact kinds. |
approved_at |
string (date-time) | null | No | null |
Set by POST .../approve. |
approved_by |
string | null | No | null |
The approving identity. |
feed_manifest |
DocumentFeedManifest |
No | empty manifest | What this document has fed into the golden translation memory. |
error |
string | null | No | null |
Raw failure message, or the exception class name. |
failure |
DocumentFailureDetail | null |
No | null |
Structured failure detail. Branch on failure.code, not on error. |
created_at |
string (date-time) | No | generated | UTC. |
updated_at |
string (date-time) | No | generated | UTC. Also the list sort key. |
DocumentTranslationRequest
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source_lang |
string | No | "en" |
Must be in the supported set. |
target_lang |
string | Yes | — | Must be in the supported set. |
domain |
string | null | No | null |
Domain hint from the domain form field. |
idempotency_key |
string | null | No | null |
From the Idempotency-Key header. |
metadata |
object | No | {} |
Server-written: content_type, size_bytes, idempotency_key, and tenant ({"id": …, "name": …}) for an authenticated tenant. |
text_type |
"Plain" | "Html" | null |
No | null |
From the text_type form field, echoed back on the job. Validated and otherwise ignored on a document job — see the note under Form fields. A value outside Plain/Html is rejected with 400 unsupported text_type …; supported values: Plain, Html. |
tone |
string | null | No | null |
From the tone form field. Register instruction applied to every segment's baseline translation. |
gender |
string | null | No | null |
From the gender form field. Grammatical gender for agreement, applied to every segment's baseline translation. |
DocumentFailureDetail
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
code |
"stage_timeout" | "transient_error" | "unexpected_error" | "interrupted" |
Yes | — | The stable value to branch on. stage_timeout and transient_error are worth retrying; unexpected_error is not; interrupted means a service restart dropped the work. |
stage |
string | Yes | — | Which stage failed: ingest, extract, map_translate, reconstruct, a review_* stage, or worker. |
message |
string | Yes | — | Exception message, falling back to the class name. |
error_type |
string | Yes | — | Exception class name. |
retry_attempts |
integer | No | 0 |
Automatic retries already spent on the failing stage. |
transient |
boolean | No | false |
Whether the runner classified the failure as retryable. |
timeout |
boolean | No | false |
Whether the stage budget was exhausted. |
metadata |
object | No | {} |
Free-form. |
DocumentFeedManifest and DocumentFedPair
feed_manifest is the durable record of what this document has contributed to the tenant's golden translation memory. It is what makes re-review and re-approve non-duplicating.
DocumentFeedManifest:
| Field | Type | Default | Description |
|---|---|---|---|
fed_pairs |
DocumentFedPair[] |
[] |
One entry per pair ever fed for this document. |
skipped |
object (string → integer) | {} |
The last approve's per-reason tally: empty, fast_path, needs_review, duplicates. Replayed verbatim by an idempotent re-approve. |
golden_skipped_at_cap |
integer | 0 |
Cumulative across this document's review and approve feeds: pairs the translation memory could not take because the language pair is at its cap (see Terminology & translation memory). Non-zero means those pairs are not memorized — free TM budget for the pair (delete or shrink an imported set) if you want them. Corrections to sources already in memory are never counted here: they are admitted even at the cap. |
last_feed_error |
string | null | null |
The most recent feed failure, if any. The feed is best-effort. A full memory is not a feed error — it lands in golden_skipped_at_cap instead. |
DocumentFedPair:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
source_text |
string | Yes | — | Segment source. |
target_text |
string | Yes | — | Segment target as fed. |
source_hash |
string | Yes | — | Normalised-source hash; the deduplication key. |
corrected |
boolean | No | false |
true when a human typed this target. |
stage |
"review" | "approve" |
No | "review" |
Whether the pair was fed by an incremental correction or by the bulk approve sweep. |
created_at |
string (date-time) | No | generated | UTC. |
DocumentQualityReport and DocumentQualityFinding
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
schema_version |
string | No | "1.0" |
|
document_id |
string | Yes | — | The layout's document_id, which is not the job id. |
source_lang / target_lang |
string | Yes | — | |
overall_score |
number | No | 1.0 |
A 0–1 fraction. Note this is a different scale from the text pipeline's 0–100 quality_assessment.score. |
translated_units |
integer | No | 0 |
Units with a translation. |
total_units |
integer | No | 0 |
Units the validator inspected. |
findings |
DocumentQualityFinding[] |
No | [] |
Per-unit validator findings. |
metadata |
object | No | {} |
e.g. page_count, schema_version, and — when per-segment quality scoring ran — llm_quality with the review counters needs_review_units and noncompliant_units. |
DocumentQualityFinding:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
severity |
"info" | "warning" | "error" |
Yes | — | Note this is a different vocabulary from the text pipeline's QualityIssue.severity (minor/major/critical). |
code |
string | Yes | — | Stable finding code. |
message |
string | Yes | — | Human-readable description. |
unit_id |
string | null | No | null |
The region or cell the finding is about. |
metadata |
object | No | {} |
Free-form. |
Finding codes
code is the stable value to branch on; message is prose written for a human and may be reworded. The codes a document job can carry:
code |
severity |
What it reports |
|---|---|---|
missing_translation |
error |
A translatable unit came back with no translated text at all. |
invalid_page_geometry |
error |
A page reports a non-positive width or height. |
invalid_region_geometry |
error |
A region reports a non-positive width or height. |
invalid_table_shape |
error |
A table reports a non-positive row or column count. |
reconstruction_text_truncated |
warning |
The translation would not fit its box even at the smallest size that fits, so trailing lines were dropped from the rendered file. The finding names the region or table cell and how much was dropped. |
reconstruction_text_shrunk |
warning |
The translation was rendered at a noticeably smaller size than the source it replaced in order to stay inside its box. The finding names both sizes. |
geometry_region_off_page |
warning |
A region's box sits entirely outside the page it belongs to — the text was translated, but its placement cannot be trusted. |
geometry_region_edge_overrun |
warning |
A region's box runs past a page edge by more than the tolerance allowed for glyph overhang, and the finding says by how far. |
geometry_region_overlap |
warning |
A region's box overlaps a neighbouring one by enough of the smaller box that one of the two is misplaced. |
extraction_geometry_rerouted |
info |
“the extracted page geometry failed validation, so the layout was re-derived with a fallback extractor (…)” — the routing gate working as intended, which is why it does not move the score. It is raised as a warning instead when re-derivation was not possible and the page had to be rendered from geometry that could not be trusted. |
missing_region_translation |
warning |
A text-bearing region — body text, heading, list, caption, header, footer or footnote — has source text but no translated text. |
missing_table_cell_translation |
warning |
A table cell has source text but no translated text. |
translation_expansion |
warning |
The translation grew far longer than its source, which is the shape that leads to shrinking or truncation at render time. |
low_segment_quality |
warning |
A segment was still scoring below the quality threshold after the pipeline had finished repairing it. |
low_segment_quality_omitted |
warning |
Per-segment low_segment_quality findings were capped, and this one counts the segments below threshold that were not listed individually. |
terminology_noncompliant |
warning |
Enforced glossary terms were still missing from a segment after repair. |
Any error-severity finding caps the report's overall_score at 0.5. warning and info findings do not move it: the file was produced and the text was translated, and what they report is fit, placement or confidence.
The layout
The extracted layout is internal to the service and is not part of any response — job.layout is always null (see What a job response contains). What crosses the API instead:
GET .../segmentsis the structured view of the document's translatable content. Its server-side walk is the single source of truth for what counts as a segment.region_idandcell_idare the layout's addressing scheme, surfaced per segment. Treat them as opaque: echo them back verbatim on corrections. Acell_idis{table_id}:{row}:{column}— a 32-character hex table id, a 0-based row and a 0-based column — but you never need to construct one.- Layout, fonts and formatting are applied server-side when the output file is produced; that is what makes the translation land in place in the downloaded document.
Reviewer identity
POST /review and POST /approve resolve the reviewer as follows:
- Try the server-derived session identity — the authenticated principal's email, falling back to its user id. It is read from the
Authorization: Beareraccess token or the browser session cookie. - If a session resolves, it wins. A different
reviewerin the body is ignored (the server logs the discrepancy) and the response echoes the authenticated identity inreview_result.reviewer/approval_result.reviewer. - If no session resolves — API-key-only clients such as a CAT tool integration — the body's
revieweris used. - If neither exists, the call fails:
{ "detail": "reviewer could not be determined: sign in or supply a reviewer" }
The practical consequence for an API-key integration: always send reviewer on /review and /approve, or the call returns 400.
Error format
The canonical error catalog — body shapes, the full error_code vocabulary, and status-code semantics — is in Conventions & errors. Below is what these endpoints emit.
HTTPException-based errors return:
{ "detail": "document job not found" }
Errors raised from the pipeline layer add a machine-readable code and echo back the X-Request-ID you sent (null when you sent none — the server-generated id is still returned in the X-Request-ID response header):
{
"detail": "document job doc_missing not found",
"error_code": "not_found",
"request_id": "c2ac6a3b91ad4608ad744a56fd74d448"
}
error_code |
HTTP status |
|---|---|
validation_error |
400 |
not_found |
404 |
conflict |
409 |
rate_limit |
429 |
transient_error |
503 |
service_unavailable |
503 |
stage_timeout |
504 |
internal_error |
500 |
Two deployment-specific codes can also surface on document calls when a tenant uses its own storage: storage_binding_not_verified (409 — verify the binding and the parked job publishes automatically) and persistence_failed (502 — the job is parked and can be retried).
Validation failures raised by FastAPI (bad enum value, out-of-range limit) use the standard 422 shape:
{
"detail": [
{
"type": "less_than_equal",
"loc": ["query", "limit"],
"msg": "Input should be less than or equal to 500",
"input": "1000",
"ctx": { "le": 500 }
}
]
}
Server flags that change what you see
| Setting | Default | Effect on these endpoints |
|---|---|---|
| Translation | on | When off, POST /api/documents/translate returns 503 translation is temporarily disabled. Reads, review, approve and download of existing jobs keep working. |
| Billing | off | When on, POST /api/documents/translate returns 402 unless the tenant holds a subscription in a current status. Document translation is never covered by the free-character trial, so a trial-only tenant gets 402 here even while text translation still works. |
| Upload cap | 30 MB | Ceiling on any request body; over it → 413. |
| Monthly character quota | per tenant | Checked at submission: POST /api/documents/translate returns 429 tenant usage limit exceeded once the tenant's billing period has spent its allowance. Enforced whether or not billing is enabled. |
| Platform storage cap | off | PLATFORM_STORAGE_CAP_BYTES, or an operator-set per-tenant override. Off by default (0 caps nothing). When set, a submission whose bytes would carry the tenant past the cap is refused with 507 before the upload is read. Skipped entirely for a tenant whose artifacts land in its own storage account. |
| Per-stage time budget | 120 s | Exceeding it fails the job with failure.code = "stage_timeout". |
| Automatic stage retries | 2 | Retries per stage before the job fails; surfaced as failure.retry_attempts. |
| Per-segment quality scoring | on | Determines whether segment.quality carries a score. With scoring off, quality carries at most the deterministic fast_path marker, never a score, so flagged_total will be 0. |
| Interrupted-job recovery | on | On startup, jobs left in a non-terminal status are marked failed with failure.code = "interrupted" and failure.stage = "worker" so pollers are not left waiting. |
The quota is measured in weighted billed characters, not raw ones: a pair touching Chinese (Simplified or Traditional), Japanese, Korean, Thai or Hindi counts each source character as 3.5 by default, on either side of the pair. See Tenants, usage & billing for the allowance formula, the top-up headroom and the remedies the 429 names.
Worked example: upload → poll → review → approve → download
BASE=https://trueidiom.com
KEY=$TRUEIDIOM_API_KEY
1. Upload. Async submit returns 202 with the job id.
DOC_ID=$(curl -sS -X POST "$BASE/api/documents/translate" \
-H "X-API-Key: $KEY" \
-H "Idempotency-Key: contract-2026-07-26-001" \
-F "source_document=@contract.pdf;type=application/pdf" \
-F "source_lang=en" \
-F "target_lang=fr" \
-F "domain=legal" | jq -r .id)
echo "$DOC_ID"
# b92fa968814843b08aea7b78f84d3894
2. Poll until terminal. Stop on translated or failed.
while true; do
STATUS=$(curl -sS "$BASE/api/documents/$DOC_ID" -H "X-API-Key: $KEY" | jq -r .status)
echo "status=$STATUS"
case "$STATUS" in
translated|failed) break ;;
esac
sleep 2
done
If failed, read the reason:
curl -sS "$BASE/api/documents/$DOC_ID" -H "X-API-Key: $KEY" | jq '.failure'
{
"code": "stage_timeout",
"stage": "map_translate",
"message": "TimeoutError",
"error_type": "TimeoutError",
"retry_attempts": 2,
"transient": true,
"timeout": true,
"metadata": {}
}
failure.message and job.error carry the raw exception message (falling back to the exception class name, as above); failure.code is the stable value to branch on — stage_timeout and transient_error are worth retrying, unexpected_error is not, and interrupted means a service restart dropped the work.
3. List the segments that need attention.
curl -sS -G "$BASE/api/documents/$DOC_ID/segments" \
-H "X-API-Key: $KEY" \
--data-urlencode "flagged=true" \
--data-urlencode "limit=100" \
--data-urlencode "offset=0" | jq '{total, flagged_total, first: .segments[0]}'
{
"total": 2,
"flagged_total": 2,
"first": {
"index": 0,
"page": 1,
"target": "region",
"region_id": "577ab4ee0485426fac76b74e22f4fdbc",
"cell_id": null,
"source": "Termination for cause",
"translated": "Résiliation pour cause",
"quality": { "needs_review": true, "terminology_compliant": false, "score": 74.0 }
}
}
Page through with offset while offset + limit < total. flagged_total stays constant, so it is safe to use as a progress denominator.
4. Correct a segment. Reuse region_id (and cell_id for table cells) exactly as returned.
curl -sS -X POST "$BASE/api/documents/$DOC_ID/review" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"reviewer": "reviewer@acme.example",
"notes": "Legal review, pass 1.",
"corrections": [
{
"target": "region",
"region_id": "577ab4ee0485426fac76b74e22f4fdbc",
"translated_text": "Résiliation pour motif valable"
}
]
}' | jq '.review_result'
{
"reviewer": "reviewer@acme.example",
"applied_corrections": 1,
"rejected_corrections": 0,
"impacted_pages": [1],
"conflict_codes": [],
"conflicts": [],
"fed_pairs": 1,
"feed_error": null
}
The output file has already been re-rendered at this point — a new translated_pdf artifact with a new checksum is appended. Re-fetch the segments to confirm the corrected unit now reports quality.corrected: true and needs_review: false.
5. Approve. This stamps the job and sweeps the clean segments into the golden translation memory.
curl -sS -X POST "$BASE/api/documents/$DOC_ID/approve" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"reviewer": "reviewer@acme.example", "notes": "Approved for delivery."}' | jq '.approval_result'
{
"approved": true,
"already_approved": false,
"reviewer": "reviewer@acme.example",
"fed_pairs": 22,
"skipped": { "fast_path": 1, "needs_review": 1, "duplicates": 0, "empty": 0 },
"feed_error": null
}
6. Download the translated file. The response is named after the source file, so rename on the way out.
curl -sS -o "contract.fr.pdf" \
"$BASE/api/documents/$DOC_ID/download" \
-H "X-API-Key: $KEY"
Approval is not a precondition for download — you can download as soon as status is translated, and again after every review re-render.