Text Translation
Start here: README · Related: Authentication · Conventions & errors · Documents · Terminology & TM
The text translation API submits plain-text source segments through the TrueIdiom human-in-the-loop pipeline: baseline translation, an automated quality gate, optional human review, and publication to the golden store and translation memory. Use this page when you are integrating string, segment, or paragraph translation — anything where you send text and read back a TranslationJob. Layout-preserving PDF/DOCX/XLSX translation is a separate pipeline under /api/documents.
Base URL: https://trueidiom.com. Every endpoint on this page is prefixed with /api. Requests take an optional api-version=2026-09-01 query parameter; omitting it serves the oldest supported version. See Versioning.
Authentication
The generated document declares no securitySchemes and no global security requirement, so the interactive /docs page shows every endpoint as unauthenticated. Auth is real, and every endpoint on this page enforces it.
Treat this page, not
/docs, as the contract. Auth is not the only thing the generated spec understates.POST /api/translationsdeclares no request body at all, so the single, batch and upload shapes documented here appear nowhere in it; theDELETEresponse body is typed only as an open object; the three file downloads across this page and Terminology & TM (GET /api/translations/{job_id}/golden,GET /api/tm/{set_id}/export,GET /api/tm/export/golden) are declaredapplication/jsonwith empty schemas, though none of them returns JSON; and several list and create responses are typed as open objects rather than as the shapes documented here.
Credentials resolve as follows for every endpoint on this page:
| Credential | Header | Where you get it |
|---|---|---|
| Tenant API key | X-API-Key: <key> |
tenant_api_key in the POST /api/auth/signup/email response, or api_key from POST /api/tenants (operator-only). Returned exactly once — see Authentication. Also accepted as Authorization: Bearer <key>. |
| OAuth2 access token | Authorization: Bearer <access_token> |
POST /api/oauth2/token — see the token endpoint. Resolves the caller's tenant. |
| Tenant selector | X-Tenant-ID: <tenant_id> |
Optional. Pins the credential to one tenant when supplied. |
Two endpoints differ. GET /api/translations/{job_id}/golden also accepts the browser session cookie (ll_session), which is what the review page's download links carry. DELETE /api/translations/{job_id} takes an administrator's access token or session cookie, or the platform operator key — never a tenant API key. See each endpoint.
Every curl example on this page uses $TRUEIDIOM_API_KEY for a tenant API key. If you do not have one yet, mint it in one call:
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 Localization","email":"you@example.com","password":"correct-horse-battery-staple"}' \
| jq -r .tenant_api_key)
A key minted this way cannot run the examples below yet. Signing up starts the free trial, and the trial covers the workspace only. With billing in force, a tenant API key on a trial tenant is refused with
402 {"detail": "the free trial covers the workspace only — API access requires an active subscription; subscribe at /billing"}, even with trial characters left and days on the clock. Subscribe at/billingfirst, or drive the examples with an OAuth2 access token fromPOST /api/oauth2/token— a bearer access token resolves as a workspace session and is admitted for as long as the trial is live. See Feature gating.
Resolution order. On a deployment that has tenant accounts, a bearer access token is tried first; if it does not resolve an active tenant, the API key is tried (from X-API-Key, falling back to the Authorization: Bearer value). X-Tenant-ID, when sent, narrows the lookup to that one account — a credential that does not belong to it is rejected rather than resolved against another. If neither credential resolves an active tenant, the request fails with 401 {"detail": "missing or invalid tenant credentials"}. On a deployment with no tenant accounts, the single configured service key is checked instead and a mismatch returns 401 {"detail": "missing or invalid API key"}.
Tenant scoping. GET /api/translations, GET /api/translations/{job_id}, POST /api/translations/{job_id}/review, GET /api/translations/{job_id}/golden and DELETE /api/translations/{job_id} are scoped to the caller's own tenant. A job belonging to another tenant is reported as 404 job not found rather than 403, so the endpoint never confirms that an id exists to a caller who cannot see it. The global_administrator role does not widen this — it is a per-tenant admin role.
Other headers.
| Header | Applies to | Effect |
|---|---|---|
X-Api-Version |
all | Alias for the api-version query parameter, for clients that would rather pin once per session than per URL. The query parameter wins when both are sent. See Versioning. |
Idempotency-Key |
POST /api/translations |
Deduplicates submissions. See Idempotency. |
X-Request-ID |
all | Echoed back on every response (generated when absent). JSON error bodies carry the same id as request_id — the value you sent, or the generated one; never null. |
X-Source-Filename |
raw-body uploads | Names the source for the flatten-to-text upload mode. |
Feature gating
Two service-level conditions change what these endpoints do. Neither is set per request.
| Condition | Caller-visible behavior |
|---|---|
| Translation is temporarily unavailable | POST /api/translations returns 503 {"detail": "translation is temporarily disabled"}. The check runs before authentication, so unauthenticated callers see the same 503. GET /api/translations, GET /api/translations/{job_id}, POST /api/translations/{job_id}/review, GET /api/translations/{job_id}/golden, and DELETE /api/translations/{job_id} are unaffected — reads, reviews, downloads, and deletions of existing jobs keep working. |
| Billing is in force | POST /api/translations requires the tenant to hold a current subscription, or to have a live free trial — characters remaining (150000) and its clock still running (14 days, counted from account creation). The free trial covers the workspace only. On a trial tenant, a caller presenting a tenant API key is refused with 402 the free trial covers the workspace only — API access requires an active subscription; subscribe at /billing even though characters and days remain; the same tenant is admitted when the call carries an OAuth2 access token, which resolves as a workspace session. A subscription lifts the distinction — both credentials work. See Errors for the six 402 bodies. While billing is not in force there are no 402 responses; usage is still recorded, and the monthly allowance (429) still applies. |
Job lifecycle
POST /api/translations runs the pipeline inline. The HTTP response is returned after the workflow finishes, and the job in that response has already reached either published (the quality gate cleared it autonomously) or awaiting_review (a human is required). GET /api/translations/{job_id} is for re-reading a job later — after a review, or from a different process.
submit ──► pending ──► translated ──► [quality gate]
│
gate passes ─┴─► published ──► golden store + TM + learning
│
gate defers ──► awaiting_review
│
review: approve ──────┼──► approved ──► published
review: modify ───────┤ └─► human_edited
review: save ─────────┤ (stays awaiting_review)
review: reject ───────┴──► rejected
JobStatus values
| Value | Meaning |
|---|---|
pending |
Job created; the workflow has not produced a baseline yet. Initial value of TranslationJob.status. A job left pending because the process running it stopped (a service restart) is marked failed, with agent_run.error job interrupted by service restart, once 10 minutes have passed since its last update — the check runs every 10 minutes — unless resending its Idempotency-Key re-runs it first (see Idempotency). |
translated |
A baseline translation exists (baseline_translation is populated). |
refined |
Present in the enum and accepted by the review endpoint as a reviewable state. The current text pipeline does not assign it; jobs move from translated to awaiting_review or published. |
awaiting_review |
The review gate opened a HumanReviewTask (agent_run.human_review_task) and is waiting for POST /api/translations/{job_id}/review. Also the resting state after a save review action. |
approved |
Transient: set the moment a reviewer approves or modifies, before the golden persist step runs. A successful persist advances the job to published/human_edited in the same request. |
human_edited |
Published with text a reviewer supplied — a modify, or an approve whose published text differs from the pipeline's own output (a differing final_translation, or a saved draft). Golden download is available. |
rejected |
A reviewer rejected the translation. Nothing is persisted to the golden store or TM. |
published |
Final translation accepted — autonomously by the quality gate, via a reviewer approve, or via a deterministic glossary/TM fast path. Golden download is available. |
failed |
The pipeline raised. The error is also on agent_run.error / agent_run.status — the API's own message for an error it reports (a content-policy refusal, an upstream outage), or internal error for an unexpected internal fault, whose details stay in the service's logs. Resending the job's Idempotency-Key re-runs it (see Idempotency). |
persistence_failed |
The translation was accepted but writing it to the golden store failed — a storage write failed, or the tenant's own storage binding was not verified at the moment of publishing (typically a review approval; new submissions are refused up front instead). The job is parked, not lost: its translation is final, readable with GET, and billed. It publishes (to published or human_edited) once the tenant verifies the binding, switches back to platform storage, or removes the binding; a tenant on platform storage retries with POST /api/storage/binding/verify. See Bring Your Own Storage. |
Only published and human_edited jobs can be downloaded from /golden. Only awaiting_review and refined jobs are reviewable.
POST /api/translations
Submit text for translation. Runs the full pipeline and returns the finished job.
Auth: required (tenant API key or bearer access token). Subject to the translation availability gate (see Feature gating).
Request body — TranslationRequest
Content-Type: application/json. A body whose Content-Type is not multipart/form-data, text/*, application/xml, text/xml, application/octet-stream, application/pdf, or the DOCX media type is parsed as JSON.
| Name | Type | Required | Description |
|---|---|---|---|
source_text |
string | Yes | Text to translate. Length is bounded by MAX_SOURCE_CHARS. Empty or whitespace-only text is refused with 400. |
target_lang |
string | Yes | Target language code. Must be in the supported set. Case-insensitive; normalized to the canonical code (ES → es). |
source_lang |
string | No | Source language code. Default "en". Same allowlist and normalization. |
idempotency_key |
string | null | No | Deduplication key. The Idempotency-Key header fills this in when the field is absent. |
domain |
string | null | No | Domain hint that steers the translation and its quality assessment. The values finance, financial, healthcare, legal, medical, pharma force the job to human review. |
metadata |
object | No | Free-form. Recognized keys are listed under Request metadata; unknown keys are stored and returned untouched. Defaults to {}. |
text_type |
"Plain" | "Html" | null |
No | "Html" tells the baseline the source is an HTML fragment: only text nodes and the human-readable title/alt/placeholder/aria-label attribute values are translated, and tags, attributes, entities and nesting are reproduced as given. See the caveat below. |
deployment_name |
string | null | No | Inert. Accepted and returned unchanged; ignored. |
adaptive_dataset_id |
string | null | No | Inert. Accepted and returned unchanged; ignored. |
allow_fallback |
boolean | null | No | Inert. Accepted and returned unchanged; ignored. |
tone |
string | null | No | Register to write the translation in (e.g. formal, informal, neutral). Reaches the baseline as a register instruction; it steers formality and word choice, never meaning. |
gender |
string | null | No | Grammatical gender to use where the target language forces a gendered form for the speaker or addressee. Governs agreement only — the translation never adds or infers a statement about a person's gender that the source does not make. |
These six fields are carried verbatim from the wire shape this API grew out of, so a client written against the old shape keeps validating. What they do has changed with the engine, and they now split in two:
- Functional —
text_type,toneandgendersteer the baseline translation. Under the older shape they applied only when the engine was specifically configured for them; today they always apply, on every request. - Inert —
deployment_name,adaptive_dataset_idandallow_fallbackhave no equivalent and are ignored. They are still accepted, stored onjob.request, and returned unchanged, so nothing breaks; supplying one logs a warning server-side. What replaces each: the baseline engine is a service-side choice, withmetadata.candidate_modelsas the per-request opt-in for extra candidates; the dataset id is replaced by the tenant's translation-memory sets, whose approved pairs inform new translations automatically; and there is no second engine left to fall back to.
Caveat on
text_type: "Html". Tag integrity is a strict instruction to the translation engine, not a structural guarantee — the engine sees the markup along with the text it is translating. Malformed, adversarial or very large fragments can still come back with altered markup, so a caller that needs a hard guarantee must validate the returned fragment itself.
These are also the per-target knobs of the batch shape, with the same functional/inert split.
Response
200 OK with a single TranslationJob object. A batch body (inputs) returns a JSON array of jobs — see Batch submission.
Example
curl -X POST "https://trueidiom.com/api/translations?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-confirm-7741-es" \
-d '{
"source_text": "Your order has shipped and will arrive on Tuesday.",
"source_lang": "en",
"target_lang": "es",
"domain": "ecommerce",
"metadata": {
"min_quality_score": 90,
"max_quality_loop_retries": 3
}
}'
import os
import requests
resp = requests.post(
"https://trueidiom.com/api/translations?api-version=2026-09-01",
headers={
"X-API-Key": os.environ["TRUEIDIOM_API_KEY"],
"Idempotency-Key": "order-confirm-7741-es",
},
json={
"source_text": "Your order has shipped and will arrive on Tuesday.",
"source_lang": "en",
"target_lang": "es",
"domain": "ecommerce",
"metadata": {"min_quality_score": 90, "max_quality_loop_retries": 3},
},
timeout=180,
)
resp.raise_for_status()
job = resp.json()
print(job["id"], job["status"])
// Node 18+
const res = await fetch("https://trueidiom.com/api/translations?api-version=2026-09-01", {
method: "POST",
headers: {
"X-API-Key": process.env.TRUEIDIOM_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "order-confirm-7741-es",
},
body: JSON.stringify({
source_text: "Your order has shipped and will arrive on Tuesday.",
source_lang: "en",
target_lang: "es",
domain: "ecommerce",
metadata: { min_quality_score: 90, max_quality_loop_retries: 3 },
}),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = await res.json();
console.log(job.id, job.status);
Response, in the tenant shape every non-operator caller receives (agent_run.steps is abbreviated to one entry — a real run carries one per workflow node):
{
"id": "9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d",
"request": {
"source_text": "Your order has shipped and will arrive on Tuesday.",
"source_lang": "en",
"target_lang": "es",
"idempotency_key": "order-confirm-7741-es",
"domain": "ecommerce",
"metadata": {
"min_quality_score": 90,
"max_quality_loop_retries": 3,
"tenant": { "id": "b41d0f9c7a2e4d18", "name": "Acme Localization" },
"terminology_gate": {
"passed": true,
"missing_terms": {},
"expected_target_counts": {},
"observed_target_counts": {},
"matched_source_terms": []
},
"golden_uri": "file:///srv/hitl/data/golden/golden-20260726.jsonl"
},
"text_type": null,
"deployment_name": null,
"adaptive_dataset_id": null,
"allow_fallback": null,
"tone": null,
"gender": null
},
"agent_run": {
"id": "2a7e5c99d1b34f0a86e2c4d7b9f1a3e5",
"workflow_name": "translation_agent_workflow",
"status": "completed",
"current_step": null,
"steps": [
{
"id": "6d4b2f8a0c1e4738b5a9d2c6e0f3a1b7",
"name": "quality_gate",
"actor": "quality_judge_agent",
"kind": "agent",
"status": "completed",
"input_summary": "Assess, repair-if-needed, verify-if-repaired.",
"output_summary": "Quality gate passed at score 93.0; no repair needed.",
"tool_calls": [],
"decisions": [],
"error": null,
"started_at": "2026-07-26T14:03:09.884120Z",
"completed_at": "2026-07-26T14:03:11.402665Z"
}
],
"quality_gate": {
"id": "c0e7b1a4d29f4c65a3b8e7d1f2c4a6b9",
"name": "human_review_gate",
"decision": "pass",
"reasons": [],
"triggers": [],
"score": 93.0,
"threshold": 90.0,
"metadata": {
"verification_passed": null,
"verification_score": null,
"assessment_score": 93.0,
"risk_flags": [],
"trigger_codes": [],
"terminology_gate": {
"passed": true,
"missing_terms": {},
"expected_target_counts": {},
"observed_target_counts": {},
"matched_source_terms": []
},
"repaired": false
},
"created_at": "2026-07-26T14:03:11.410882Z"
},
"human_review_task": null,
"error": null,
"started_at": "2026-07-26T14:03:07.221904Z",
"completed_at": "2026-07-26T14:03:12.663017Z"
},
"baseline_translation": "Tu pedido ha sido enviado y llegará el martes.",
"quality_assessment": {
"score": 93.0,
"issues": [],
"reasoning": "Accurate and fluent; register matches the source.",
"created_at": "2026-07-26T14:03:11.398441Z"
},
"refinement": null,
"verification": null,
"final_translation": "Tu pedido ha sido enviado y llegará el martes.",
"reviewer": null,
"review_action": null,
"review_notes": null,
"learning_signals": [
{
"signal_type": "translation_memory",
"source_lang": "en",
"target_lang": "es",
"domain": "ecommerce",
"reviewer": null,
"source_text": "Your order has shipped and will arrive on Tuesday.",
"before": "Tu pedido ha sido enviado y llegará el martes.",
"after": "Tu pedido ha sido enviado y llegará el martes.",
"confidence": 1.0,
"evidence": { "review_action": null },
"created_at": "2026-07-26T14:03:12.514773Z"
}
],
"status": "published",
"created_at": "2026-07-26T14:03:07.220188Z",
"updated_at": "2026-07-26T14:03:12.661440Z"
}
Errors
| Status | detail |
Cause |
|---|---|---|
400 |
invalid Content-Length header |
Content-Length is not an integer. |
400 |
invalid JSON body: … |
Body did not parse as JSON. |
400 |
JSON body must be an object |
Body parsed to a non-object (array, string, number). |
400 |
source_text has no translatable content |
source_text is empty or whitespace only. On the batch shape the detail names the item — inputs[3].Text has no translatable content — and one blank item rejects the whole request. Nothing is translated or billed. |
400 |
source_document upload is required |
A multipart/form-data body with no source_document file part — including one that sends the field as a plain form value instead of a file. |
400 |
source_document must be UTF-8 or UTF-16 (with BOM) text |
A flatten-to-text upload in any other encoding (Windows-1252, Shift-JIS, UTF-16 without a byte-order mark, UTF-32, …), or text containing a NUL character. Nothing is translated. |
400 |
source_document is a password-protected PDF; remove the password and upload it again / source_document is not a valid PDF file: … / source_document is not a valid DOCX file: … |
A PDF or DOCX sent as a raw body (the flatten-to-text shape) that is encrypted or cannot be read. |
400 |
source_document could not be read as PDF: … (likewise DOCX, XLSX) — error_code: validation_error |
A layout-document upload with wait set whose file is corrupt, password-protected, or not the format it claims. Without wait the job is accepted and fails at extraction instead — see Documents. |
400 |
source_document unpacks to … bytes, over the …-byte limit for Office documents; split it into smaller files (and the other Office-document limits) |
A DOCX or XLSX over the Office-document limits, on the layout-document shape or as a raw-body DOCX. Which limits apply, and when each is checked: Supported uploads. |
401 |
missing or invalid tenant credentials / missing or invalid API key |
See Authentication. |
402 |
an active subscription is required to run translations — add a payment method at /billing |
Billing is in force and the tenant has no current subscription and no trial grant. |
402 |
the free trial covers the workspace only — API access requires an active subscription; subscribe at /billing |
The tenant is on a live trial (characters left, clock still running) and the call presented a tenant API key. The trial surface is the workspace session, so the key waits for a subscription; an OAuth2 access token on the same tenant is admitted. |
402 |
your free trial is used up — add a payment method at /billing to continue |
Trial grant exhausted. Checked before the clock, so a spent grant always reads this way. |
402 |
your free trial has ended — add a payment method at /billing to continue |
The trial's calendar clock (14 days from account creation) ran out with characters still on the grant. |
402 |
this request exceeds your remaining free trial characters — add a payment method at /billing |
This request's billable characters exceed the remaining trial balance. Counted before any work starts, and weighted — see Limits. |
402 |
document translation (PDF, DOCX, XLSX) is not included in the free trial — it requires an active subscription; subscribe at /billing |
A layout-document upload from a tenant whose free trial is still live. The trial covers text only. |
409 |
workspace storage (BYOS) binding is not verified — verify it in Storage settings and resubmit, or switch the workspace back to platform storage |
error_code: storage_binding_not_verified. The tenant's own storage binding is not verified — or cannot be used: its stored secret is missing or unreadable, or its stored account URL no longer passes validation. Checked before any translation work, on every shape — including work that would have gone to review — so no job is created and nothing is billed: fix and verify the binding (re-entering the secret if it is gone), then resubmit. Layout-document uploads get the same 409 before a job exists. If the binding stops verifying while a job is already being published, that job is parked as persistence_failed instead, and the detail says it will publish automatically. |
413 |
request body exceeds MAX_REQUEST_BYTES |
Declared Content-Length above 10,000,000 bytes on a JSON, raw-body or flatten-to-text submission. A PDF/DOCX/XLSX upload is not held to it — see the next row. |
413 |
request body exceeds MAX_UPLOAD_BYTES |
Body above the absolute app-wide ceiling of 30,000,000 bytes — the bound a PDF/DOCX/XLSX upload is held to, as on POST /api/documents/translate. |
413 |
batch exceeds MAX_BATCH_ITEMS |
See Limits. |
413 |
source text exceeds MAX_SOURCE_CHARS |
See Limits. |
415 |
unsupported source_document type |
Upload mode with an unsupported file type. |
422 |
unsupported language(s): 'xx'. Supported: en, es, fr, … |
source_lang or target_lang outside the allowlist or not a string (including a missing target_lang). |
422 |
The validation message as a single string, e.g. 1 validation error for TranslationRequest … |
Body failed model validation (e.g. source_text missing, a text_type outside Plain/Html, a malformed batch). detail is a string here, not the HTTPValidationError list. |
429 |
tenant usage limit exceeded |
This request's weighted source characters, added to the tenant's recorded usage and to its other work still in flight (text requests and document jobs), would exceed its monthly allowance — or would take the calendar month past its character ceiling. For a subscribed tenant the detail continues with the remedy it has: a top-up for this month, or an upgrade, which raises the allowance at once (a downgrade waits for renewal), or contact us at /contact when neither can add characters (Quotas). |
502 |
golden persistence failed: … — the job is parked as persistence_failed and can be retried |
error_code: persistence_failed. Writing the translation to the golden store failed — a storage outage (on platform storage the reason reads platform storage is temporarily unavailable; on the tenant's own storage it is that account's answer), or the tenant's stored secret disappearing while this job was already being published (a submission is otherwise refused up front with 409). The job is parked and billed; see persistence_failed. A parked job deleted while its request was still running answers 404 not_found instead, and is not billed. |
503 |
translation is temporarily disabled |
Translation is temporarily unavailable. |
503 |
secret storage is temporarily unavailable — try again shortly |
error_code: transient_error. The service's own secret storage could not be read to check the tenant's storage binding. Nothing was created or billed; retry shortly. |
507 |
platform storage cap reached (… of … bytes used) — … |
A layout-document upload that would take the tenant past its platform storage cap — the same admission as POST /api/documents/translate (The 507 admission response). |
Pipeline failures surface through the shared error handler with {"detail", "error_code", "request_id"}: 429 rate_limit (upstream translation capacity is saturated — retry after a pause), 503 transient_error (the model provider was unavailable — retry), 422 content_filtered (the model provider's content policy refused the text — the same text is refused again on every retry), and 404 not_found when the job was deleted while its pipeline ran (see DELETE /api/translations/{job_id}). A layout-document upload with wait set runs the document pipeline inline and can also return its errors, among them 429 quota_exceeded and 504 stage_timeout — see Documents. Any other failure is an unexpected internal one: 500 with {"detail": "internal error", "error_code": "internal_error", "request_id": "<id>"} and the same id in the X-Request-ID header. A job whose pipeline raised is saved with status: "failed" and can still be read with GET /api/translations/{job_id}; resending its Idempotency-Key re-runs it.
Order of checks. The translation switch (503), the declared body size of a JSON or raw-body submission (413), credentials (401), then the body itself — 400 for JSON that does not parse or is not an object, 422 for an unsupported source_lang or target_lang (a batch's languages are checked with the rest of the batch, below) — come first, so a malformed request is told so even by a tenant that could not pay for it. A keyed retry of work that already exists is answered next (Idempotency). Then billing (402), the monthly allowance (429) and the tenant's own storage (409/503); the remaining payload checks — source length and batch size (413), blank text (400), the request model (422) — run after those.
Other request shapes accepted by this endpoint
POST /api/translations is content-type polymorphic. Besides the JSON TranslationRequest above:
| Shape | Trigger | Result |
|---|---|---|
| Batch | JSON body containing inputs |
Array of TranslationJob. See Batch submission. |
| Layout document | multipart/form-data with a source_document file whose format is a layout format handled by the document service (PDF/DOCX/XLSX), recognised by its file extension or, failing that, by the part's content type |
Routed to the document pipeline; returns a document job (TenantDocumentJob). Poll GET /api/documents/{id} and download from GET /api/documents/{id}/download — see Documents. Never covered by the free trial — an active subscription is required when billing is enabled. Admitted like POST /api/documents/translate — 429 when the month has no headroom left, 507 at the platform storage cap, 409 while the tenant's own storage binding is not verified or cannot be used — and held to the same 30,000,000-byte upload limit, not this endpoint's 10,000,000-byte declared-size limit for text. |
| Flatten-to-text upload (multipart) | multipart/form-data with a source_document of type .txt, .md, .html, .htm, .csv, .json, .xml, or a text/* content type |
Text is extracted and split into pages — at form feeds, else at Page N lines, else by lines — and any page, or single line, longer than 5,000 characters is split further, so no page exceeds 5,000 characters. Each page is submitted as its own text job. A text file must be UTF-8 (a BOM is fine) or UTF-16 with a BOM; anything else is refused with 400 and nothing is translated. Returns the document-mode object below. |
| Flatten-to-text upload (raw body) | A raw body with Content-Type: text/*, application/xml, text/xml, application/octet-stream, application/pdf, or the DOCX media type |
Same per-page flow; every parameter is a query parameter. |
JSON with metadata.source_document |
JSON body where metadata.source_document is an object |
Same per-page flatten-to-text flow as above. |
Layout-document form fields
| Name | Type | Required | Description |
|---|---|---|---|
source_document |
file | Yes | PDF, DOCX or XLSX. |
target_lang |
string | Yes | Target language code. |
source_lang |
string | No | Default en. |
domain |
string | No | Domain hint. |
tone |
string | No | Register instruction applied to every segment's baseline translation. Blank or absent means no register instruction, not a default. |
gender |
string | No | Grammatical gender for agreement, applied to every segment's baseline translation. |
text_type |
string | No | Accepted and echoed on the job, but only Plain does anything: PDF/DOCX/XLSX carry no HTML markup, so Html is logged once as ignored and never affects the translation. Anything outside Plain/Html → 400. See POST /api/documents/translate. |
wait |
string | No | 1, true or yes runs the document pipeline inline, so the returned job is already finished; anything else queues the job and responds as soon as it is accepted. Either way the status code is 200 — this endpoint never signals 202, so branch on the returned job's status, not on the status code. (POST /api/documents/translate is the endpoint that answers 202 for queued work.) |
Flatten-to-text form fields (multipart)
| Name | Type | Required | Description |
|---|---|---|---|
source_document |
file | Yes | Text-bearing file. Zero bytes → 400 source_document is empty. |
target_lang |
string | Yes | Target language code. This mode has no required-field check of its own: a blank or absent value is carried through as the empty string and rejected by the language allowlist as 422 unsupported language(s): ''. Supported: en, es, fr, … — not as a 400. |
source_lang |
string | No | Default en. |
domain |
string | No | Domain hint. |
text_type |
string | No | Default Plain. Html applies the HTML-fragment handling described under TranslationRequest. |
tone |
string | No | Register instruction applied to every page's baseline translation. Blank or absent means no register instruction, not a default. |
gender |
string | No | Grammatical gender for agreement, applied to every page's baseline translation. |
deployment_name |
string | No | Inert — accepted and ignored. Supplying any one of deployment_name, adaptive_dataset_id or allow_fallback still sets all three on every page's request (allow_fallback then defaults to true if you did not send it), so the round-trip shape is unchanged; none of the three affects the translation. |
adaptive_dataset_id |
string | No | Inert. Same grouping rule as deployment_name. |
allow_fallback |
boolean-ish string | No | Inert. Same grouping rule as deployment_name. |
candidate_models |
string | No | Comma-separated. Opts every page into the extra-candidate path described under metadata.candidate_models. Populates metadata.candidate_models, and its first entry becomes metadata.primary_model; neither key is echoed back in the tenant response shape. |
model |
string | No | Single-value alternative to candidate_models, merged into the same list. |
min_quality_score |
number | No | Populates metadata.min_quality_score on every page's request. |
Flatten-to-text query parameters (raw body)
Raw-body mode has no form to read, so the same knobs arrive as query parameters. The source text is the request body itself.
| Name | Type | Required | Description |
|---|---|---|---|
target_lang |
string | Yes | Blank or absent → 400 target_lang query parameter is required for text uploads. |
source_lang |
string | No | Default en. |
domain |
string | No | Domain hint. |
text_type |
string | No | Default Plain. |
tone |
string | No | Register instruction applied to every page's baseline translation. Parity with the multipart form. |
gender |
string | No | Grammatical gender for agreement. Parity with the multipart form. |
deployment_name |
string | No | Inert — accepted and ignored. Same grouping rule as the multipart form. |
adaptive_dataset_id |
string | No | Inert. |
allow_fallback |
boolean-ish string | No | Inert. |
candidate_models |
string | No | Comma-separated → metadata.candidate_models and metadata.primary_model. Same extra-candidate opt-in as the multipart form. |
model |
string | No | Single-value alternative to candidate_models. |
min_quality_score |
number | No | → metadata.min_quality_score. |
filename |
string | No | Names the source. Overridden by the X-Source-Filename header; falls back to source_document.txt. |
curl -X POST "https://trueidiom.com/api/translations?target_lang=fr&source_lang=en&min_quality_score=92&api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: text/plain; charset=utf-8" \
-H "X-Source-Filename: handbook.txt" \
--data-binary @handbook.txt
Document-mode response:
{
"mode": "document",
"source_document": "handbook.txt",
"target_lang": "fr",
"pages": 3,
"job_ids": [
"4f8a1c6d2b9e4703a5c8d1f2e3b4a5c6",
"7b3e9d0c5a1f42689d0e3b7c2a5f8d14",
"1d6c4b8e0f2a47539c7e1b5d3f0a2c68"
],
"first_job_id": "4f8a1c6d2b9e4703a5c8d1f2e3b4a5c6",
"output_file": null
}
output_file is always null: no output file is written, and the key stays only so the response shape does not change within this API version. To retrieve translated content, read each id in job_ids with GET /api/translations/{job_id} or download it from GET /api/translations/{job_id}/golden.
Pages run in order, and each is billed as it completes. If a page fails, the request returns that page's error; the pages before it are kept, billed and readable. With an Idempotency-Key — or, on the JSON metadata.source_document shape, an idempotency_key in the body, which takes precedence — page n is keyed <value>:n (counting from 1), so a retry with the same key returns the finished pages and runs the rest.
413 document exceeds MAX_BATCH_ITEMS when split into pages is returned when the page count exceeds 50; 400 source_document has no textual content when an upload yields no text (a blank source_text on the JSON metadata.source_document shape gets 400 source_document has no translatable content).
GET /api/translations
List the calling tenant's translation jobs, most recently updated first.
Auth: required.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
status |
JobStatus | null |
No | Filter by status. One of pending, translated, refined, awaiting_review, approved, human_edited, rejected, published, failed, persistence_failed. Omit for all statuses. |
Example
curl -G "https://trueidiom.com/api/translations?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
--data-urlencode "status=awaiting_review"
import os
import requests
resp = requests.get(
"https://trueidiom.com/api/translations?api-version=2026-09-01",
headers={"X-API-Key": os.environ["TRUEIDIOM_API_KEY"]},
params={"status": "awaiting_review"},
timeout=60,
)
resp.raise_for_status()
for job in resp.json():
print(job["id"], job["status"])
// Node 18+
const url = new URL("https://trueidiom.com/api/translations?api-version=2026-09-01");
url.searchParams.set("status", "awaiting_review");
const res = await fetch(url, {
headers: { "X-API-Key": process.env.TRUEIDIOM_API_KEY },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
for (const job of await res.json()) {
console.log(job.id, job.status);
}
[
{
"id": "9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d",
"request": { "source_text": "Your order has shipped.", "source_lang": "en", "target_lang": "es", "idempotency_key": null, "domain": "ecommerce", "metadata": {}, "text_type": null, "deployment_name": null, "adaptive_dataset_id": null, "allow_fallback": null, "tone": null, "gender": null },
"agent_run": { "id": "2a7e5c99d1b34f0a86e2c4d7b9f1a3e5", "workflow_name": "translation_agent_workflow", "status": "waiting_for_human", "current_step": null, "steps": [], "quality_gate": null, "human_review_task": null, "error": null, "started_at": "2026-07-26T14:03:07.221904Z", "completed_at": null },
"baseline_translation": "Tu pedido ha sido enviado.",
"quality_assessment": null,
"refinement": null,
"verification": null,
"final_translation": "Tu pedido ha sido enviado.",
"reviewer": null,
"review_action": null,
"review_notes": null,
"learning_signals": [],
"status": "awaiting_review",
"created_at": "2026-07-26T14:03:07.220188Z",
"updated_at": "2026-07-26T14:03:12.661440Z"
}
]
The full response returns whole TranslationJob objects in the shape described under TranslationJob; the example above elides agent_run.steps and quality_gate for brevity.
Errors
| Status | Cause |
|---|---|
401 |
Missing or invalid credentials. |
422 |
status is not a JobStatus value. |
GET /api/translations/{job_id}
Read one translation job, including the full agent run, quality evidence, and review state.
Auth: required. Jobs owned by another tenant return 404.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | The id returned by POST /api/translations. |
Example
curl "https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
import os
import requests
resp = requests.get(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d?api-version=2026-09-01",
headers={"X-API-Key": os.environ["TRUEIDIOM_API_KEY"]},
timeout=60,
)
resp.raise_for_status()
job = resp.json()
print(job["status"], job["final_translation"])
// Node 18+
const res = await fetch(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d?api-version=2026-09-01",
{ headers: { "X-API-Key": process.env.TRUEIDIOM_API_KEY } },
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = await res.json();
console.log(job.status, job.final_translation);
Returns the same TranslationJob shape shown above.
Errors
| Status | detail |
Cause |
|---|---|---|
401 |
missing or invalid tenant credentials |
No usable credentials. |
404 |
job not found |
Unknown id, or a job owned by another tenant. |
POST /api/translations/{job_id}/review
Submit a human decision on a job that is waiting for review. This is the endpoint that publishes a translation to the golden store and translation memory.
Auth: required. Ownership is checked before the review is applied, so a caller from another tenant gets 404 and cannot approve, edit, or reject someone else's job.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | Job to review. Must be in status awaiting_review or refined. |
Request body — ReviewSubmission
| Name | Type | Required | Description |
|---|---|---|---|
action |
"save" | "approve" | "modify" | "reject" |
Yes | The reviewer decision. See the table below. |
reviewer |
string | Yes | Required. Identity recorded on the job as job.reviewer and stamped on the completed HumanReviewTask as completed_by. Send the reviewer's email address or a stable internal user id. |
final_translation |
string | null | Conditional | Required for modify. Optional for save (stores a draft) and for approve (applied as a reviewer edit when it differs from the pipeline's candidate). Ignored for reject. |
notes |
string | null | No | Free text stored on job.review_notes and attached to the review decision. |
Action semantics
action |
final_translation |
Resulting status | Golden store / TM / learning |
|---|---|---|---|
save |
Optional — stored as a draft; when omitted the current best candidate is kept | awaiting_review (unchanged) |
Not run. The review task stays open. |
approve |
Optional. Omitted, blank, or identical to the current candidate (final_translation — the pipeline's output, or the latest saved draft — else refinement.refined_translation, else baseline_translation): the candidate is published as-is. A differing value is a reviewer edit and is published instead. Whenever the published text differs from the pipeline's own output — a differing value, or a saved draft approved as-is — the job lands as human_edited, exactly as if modify had been used; otherwise published. (The review UI posts the textarea content on every action, so an untouched textarea round-trips as a plain approval.) |
approved → published, or human_edited when the published text is a reviewer's |
Persisted, TM dual-write, learning signals emitted. |
modify |
Required | approved → human_edited |
Persisted, TM dual-write, learning signals emitted (including a reviewer_edit signal capturing the before/after). |
reject |
Ignored | rejected |
Not run. The review task is closed. |
What happens on approve/modify
job.reviewer,job.review_action, andjob.review_notesare recorded, and the openHumanReviewTaskis closed with the reviewer's name.- The final translation is re-checked against the tenant glossary. The result is written to
request.metadata.terminology_gate. If a reviewer publishes output that is not terminology-compliant, the override is recorded atrequest.metadata.terminology_gate_override({"overridden": true, "by": …, "action": …, "missing_terms": {…}}). - The job is persisted to the golden store; the returned URI is written to
request.metadata.golden_uri. If persistence fails, the job is parked aspersistence_failedand the request returns502(or409for an unverified BYOS binding); it publishes on its own once storage is healthy again — seepersistence_failed. - The approved
(source_text, final_translation)pair is dual-written into the tenant's golden translation memory set. TM failures are swallowed — they never fail an approval that already reached the golden store. The same is true of a full memory: once the tenant has reached its segment limit for the language pair across all its sets, a pair whose source is new to the golden set is skipped rather than stored, and the approval still returns200. A correction to a source already in memory is admitted even at the cap. See Terminology & translation memory. The golden TM set is readable throughGET /api/tm/export/golden. - Learning signals are extracted onto
job.learning_signals. There is no retraining step: the pair written in step 4 is already live, and the next request in that language pair can reuse it on an exact match or draw on it as guidance. - The status advances to
published(approve) orhuman_edited(modify, or an approve that published a reviewer's text), andagent_run.statusbecomescompleted.
Example
curl -X POST "https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/review?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "modify",
"reviewer": "maria.lopez@acme.example",
"final_translation": "Su pedido ha sido enviado y llegará el martes.",
"notes": "Formal register required for this account."
}'
import os
import requests
resp = requests.post(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/review?api-version=2026-09-01",
headers={"X-API-Key": os.environ["TRUEIDIOM_API_KEY"]},
json={
"action": "modify",
"reviewer": "maria.lopez@acme.example",
"final_translation": "Su pedido ha sido enviado y llegará el martes.",
"notes": "Formal register required for this account.",
},
timeout=60,
)
resp.raise_for_status()
job = resp.json()
print(job["status"], job["review_action"])
// Node 18+
const res = await fetch(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/review?api-version=2026-09-01",
{
method: "POST",
headers: {
"X-API-Key": process.env.TRUEIDIOM_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "modify",
reviewer: "maria.lopez@acme.example",
final_translation: "Su pedido ha sido enviado y llegará el martes.",
notes: "Formal register required for this account.",
}),
},
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const job = await res.json();
console.log(job.status, job.review_action);
Response — the updated TranslationJob (fields elided for brevity; the endpoint returns the complete object):
{
"id": "9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d",
"final_translation": "Su pedido ha sido enviado y llegará el martes.",
"reviewer": "maria.lopez@acme.example",
"review_action": "modify",
"review_notes": "Formal register required for this account.",
"status": "human_edited",
"updated_at": "2026-07-26T14:21:44.902117Z"
}
Errors
| Status | detail / error_code |
Cause |
|---|---|---|
400 |
`final_translation` is required for `modify` — error_code: validation_error |
modify without a replacement translation. |
400 |
Job not reviewable in status JobStatus.PUBLISHED — error_code: validation_error |
The job is not in awaiting_review or refined. |
401 |
missing or invalid tenant credentials |
No usable credentials. |
404 |
job not found |
Unknown id, or a job owned by another tenant. |
404 |
translation job … not found — error_code: not_found |
Raised by the orchestrator when the job does not exist and the tenant ownership pre-check did not already reject it (a platform operator's call, deployments with no tenant accounts, or a job deleted between the two lookups). |
404 |
translation job … was deleted during review — error_code: not_found |
An admin deleted the job while this review was running. The delete stands, and whatever the review had already written to the golden store and TM is removed. |
409 |
workspace storage (BYOS) binding is not verified — … |
Golden persist blocked by an unverified storage binding. The job is parked as persistence_failed and publishes automatically once the binding verifies. |
422 |
HTTPValidationError body |
action is not one of the four values, or reviewer is missing. |
502 |
golden persistence failed: … |
Golden store write failed; the job is parked as persistence_failed. |
GET /api/translations/{job_id}/golden
Download the published translation as a file.
Auth: required — tenant credentials (a tenant API key or a bearer access token), or the browser session cookie (ll_session), which is what the review page's download links carry. With none of these the request is refused with 401 missing or invalid tenant credentials before the job is looked up, so an anonymous caller learns nothing about which ids exist; credentials that are sent but do not resolve (a bad token, or an X-Tenant-ID the credential does not belong to) are refused the same way. Another tenant's job returns 404. Only the platform operator crosses tenants.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | Job to download. Must be in status published or human_edited. |
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
format |
string | No | txt (default), docx, or pdf. Values outside this set are rejected by the ^(txt|docx|pdf)$ pattern. |
Response
200 OK with the file as an attachment.
format |
Content-Type |
Filename |
|---|---|---|
txt |
text/plain; charset=utf-8 |
<stem>.txt |
docx |
application/vnd.openxmlformats-officedocument.wordprocessingml.document |
<stem>.docx |
pdf |
application/pdf |
<stem>.pdf |
<stem> is <sanitized source name>-<target_lang>. The source name comes from request.metadata.source_document.name when the job came from an upload, otherwise it is source_document — e.g. source_document-es.txt, handbook-fr.docx. Sanitizing drops the name's extension and replaces each run of characters other than ASCII letters, digits, ., _ and - with a single _; a name with nothing left becomes source_document.
For an Arabic, Persian or Urdu target, the docx paragraphs are set right to left.
Example
curl -OJ "https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/golden?format=docx&api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
import os
import requests
resp = requests.get(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/golden?format=docx&api-version=2026-09-01",
headers={"X-API-Key": os.environ["TRUEIDIOM_API_KEY"]},
timeout=120,
)
resp.raise_for_status()
# curl -OJ saves under the server-supplied filename; name it yourself here.
with open("translation.docx", "wb") as handle:
handle.write(resp.content)
// Node 18+
import { writeFile } from "node:fs/promises";
const res = await fetch(
"https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/golden?format=docx&api-version=2026-09-01",
{ headers: { "X-API-Key": process.env.TRUEIDIOM_API_KEY } },
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
// curl -OJ saves under the server-supplied filename; name it yourself here.
await writeFile("translation.docx", Buffer.from(await res.arrayBuffer()));
Errors
| Status | detail |
Cause |
|---|---|---|
401 |
missing or invalid tenant credentials |
No tenant credentials and no session cookie, or credentials that do not resolve. Checked before the job is looked up. |
404 |
job not found |
Unknown id, or another tenant's job. |
404 |
golden translation is empty |
The job has no final_translation. |
409 |
golden translation is not available for this job status |
The job is not published or human_edited — including approved, awaiting_review, rejected, and persistence_failed. |
422 |
HTTPValidationError body |
format is not txt, docx, or pdf. |
DELETE /api/translations/{job_id}
Hard-delete one text job and everything it produced (self-service content deletion).
Auth: admin only — a signed-in global_administrator of the job's own tenant (bearer access token or the ll_session cookie), or the platform operator API key. A tenant API key cannot delete (401). A non-admin member gets 403; an admin of another tenant gets 404 (deletion never confirms a foreign job id exists). Ownership is judged against the signed-in admin's own tenant — X-Tenant-ID plays no part. Only the platform operator crosses tenants.
The delete cascades. Removed along with the job record:
- the golden system-of-record record(s) the job wrote on approval/publish (platform store, and the tenant's own BYOS golden store when one is bound),
- the translation-memory pair the job fed into the tenant's
golden-approved-<src>-<tgt>set, matched by normalized source hash. Only that pair is removed — sibling pairs and client TMX imports are never touched. A job that never fed the TM (unapproved, rejected, or published via a TM/glossary fast path that replayed an existing pair) removes no pairs.
Deliberately retained: usage/billing records and the auth audit log survive deletion (legal/tax retention — see the privacy policy).
Learning rows: the response always reports learning_rows_removed: 0 for text jobs. Persisted learning rows carry no job id and the storage is not tenant-keyed, so no row can be tied to a deleted text job with certainty; rather than risk a wrong cross-tenant delete, nothing is removed there.
A job that is still running. A delete that lands while the job's pipeline — or a review of it — is still running is honoured: the job is not re-created when that run finishes, whatever the run wrote to the golden store and translation memory in the meantime is removed as well, and the in-flight POST /api/translations (or POST /api/translations/{job_id}/review) returns 404 with error_code: not_found — translation job <id> was deleted while it was processing (or … was deleted during review). A job deleted while its own POST /api/translations was still running is not billed — unless its golden record had already reached the tenant's own storage (Bring Your Own Storage) before the delete: the customer then holds the translation in their account, so it is billed like any delivered translation, even though the request answered 404. On platform storage the delete removes the only copy, and nothing is billed.
After deletion, GET /api/translations/{job_id} and the golden download GET /api/translations/{job_id}/golden both return 404. Deletion is permanent — there is no undo.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
job_id |
string | Yes | Job to delete. |
Response
200 OK. Counts report what was actually removed:
{
"deleted": true,
"job_id": "9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d",
"golden_record_deleted": true,
"tm_pairs_removed": 1,
"learning_rows_removed": 0
}
Example
curl -X DELETE "https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d?api-version=2026-09-01" \
-H "Authorization: Bearer $TRUEIDIOM_ACCESS_TOKEN"
Errors
| Status | detail |
Cause |
|---|---|---|
401 |
admin credentials required |
No admin credentials — including a tenant API key, which cannot delete. |
403 |
admin role required |
Authenticated, but not a global_administrator. |
404 |
job not found |
Unknown id, another tenant's job, or an already-deleted job (double delete). |
Schemas
TranslationJob
Returned by POST /api/translations (single or in an array), GET /api/translations, GET /api/translations/{job_id}, and POST /api/translations/{job_id}/review.
| Field | Type | Description |
|---|---|---|
id |
string | Job id (32-character hex). Server-generated. |
request |
TranslationRequest |
The submitted request, plus metadata the pipeline wrote back. Always present. |
agent_run |
AgentRun |
Full workflow trace: id, workflow_name (translation_agent_workflow), status (pending/running/waiting_for_human/completed/failed), current_step, steps[], quality_gate, human_review_task, error, started_at, completed_at, plus metadata (operator only). |
baseline_translation |
string | null | The first-pass translation, before any repair. |
quality_assessment |
QualityAssessment | null |
score (0–100), issues[], reasoning, created_at, plus model and usage (operator only). |
refinement |
Refinement | null |
Populated only when the job was automatically repaired: score, refined_translation, issues[], reasoning, created_at, plus model and usage (operator only). |
verification |
VerificationResult | null |
Populated only when a repair happened: passed, score, issues[], checks ({name: bool}), risk_flags[], reasoning, created_at, plus model and usage (operator only). |
final_translation |
string | null | The translation to ship. Set by the quality gate, by a reviewer, or by a glossary/TM fast path. |
reviewer |
string | null | Identity recorded by the review submission. null until a review happens. |
review_action |
"save" | "approve" | "modify" | "reject" | null |
Last review action. |
review_notes |
string | null | Reviewer notes. |
learning_signals |
LearningSignal[] |
Emitted on approval. Each has signal_type (translation_memory, reviewer_edit, terminology_preference, style_preference, quality_override, retraining_readiness, document_approved), source_lang, target_lang, domain, reviewer, source_text, before, after, confidence, evidence, created_at. Empty until the job is approved. retraining_readiness is no longer emitted — it belonged to a retired retraining hook — but remains a valid enum member so jobs stored before its removal still deserialize; treat it as read-only history. |
status |
JobStatus |
See Job lifecycle. Default pending. |
created_at |
string (date-time) | UTC, e.g. 2026-07-26T14:03:07.220188Z. |
updated_at |
string (date-time) | UTC. |
Two response shapes. A platform operator (the operator API key, or a designated operator session) gets the stored job exactly as written. Every other caller — tenant API keys and sessions alike, including a per-tenant global_administrator — gets the tenant shape, in which the pipeline's execution telemetry is absent, not null:
- no
modeland nousageonquality_assessment,refinementorverification; theirscore,issues[],reasoning,created_at— andrefined_translation,passed,checks,risk_flags— are unchanged; - no
metadataobject anywhere underagent_run— not on the run, itssteps[], theirtool_calls[], or theirdecisions[]— while the narrative those carry (names, statuses, input/output summaries, decision rationale, timestamps) stays; agent_run.quality_gate.metadatakeepsassessment_score,verification_score,verification_passed,risk_flags,trigger_codes,terminology_gate,repairedandfast_path, but not the operator-only execution trace;- no
candidate_modelsorprimary_modelechoed back inrequest.metadata.
Content, reviews, learning signals, scores and reasoning are identical in both shapes, and nothing withheld is billing data: billing is metered on source characters, weighted per language pair (see Limits), never on the token counts these fields carry. The tenant shape is the one /openapi.json publishes for these routes, and every example on this page shows it. See Who sees the token ledger.
Two nested objects matter most when deciding what to do next:
agent_run.quality_gate(QualityGate) —decisionispass,needs_human, orfail;scoreandthresholdare the numbers the decision was made on;triggers[]lists everything the gate raised andreasons[]is their messages, one per trigger and in the same order. Both are stamped whatever the decision was, so they are not a review signal: apassgate can carry a non-emptyreasons[]andtriggers[]— only ablockingtrigger forces review, while awarningone (terminology_uncertainty,risk_flag) rides along on a job that publishes autonomously, and a fast-pathpasscarries a one-linereasons[]withtriggers[]empty. Branch ondecisionand onstatus, never on whether these arrays are empty.metadatacarriesassessment_score,verification_score,verification_passed,risk_flags,trigger_codes,terminology_gate,repaired,fast_pathwhen a fast path served the job, and — for an operator only — the execution trace.agent_run.human_review_task(HumanReviewTask) — present when a human is required.
HumanReviewTask
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | generated | 32-character hex task id. |
status |
"open" | "completed" | "cancelled" |
open |
completed after POST /api/translations/{job_id}/review closes it. |
requested_by |
string | review_gate_agent |
Which component opened the task. |
reason |
string | — | Required. Display-ready summary, e.g. Human review requested: 1 warning trigger(s). |
question |
string | null | null |
Display-ready question for the reviewer. |
instructions |
string | null | null |
Optional extra guidance for the reviewer. |
requested_actions |
array of "save" | "approve" | "modify" | "reject" |
all four | The actions the review endpoint will accept for this task. |
triggers |
HumanReviewTrigger[] |
[] |
Why review was requested. |
completed_by |
string | null | null |
The reviewer value from the review submission. |
created_at |
string (date-time) | generated | UTC. |
completed_at |
string (date-time) | null | null |
Set when the task closes. |
metadata |
object | {} |
Free-form. |
Human-review trigger codes (HumanReviewTrigger.code, each with severity of info, warning, or blocking): policy_required, low_quality_score, critical_issue, terminology_uncertainty, terminology_noncompliant, sensitive_content, high_repair_delta, verification_failed, risk_flag.
QualityIssue
Appears as quality_assessment.issues[], refinement.issues[], and verification.issues[]. Both enums are load-bearing: the autonomous-publish gate refuses to ship a candidate carrying any issue with severity: "critical".
| Field | Type | Required | Description |
|---|---|---|---|
severity |
"minor" | "major" | "critical" |
Yes | A single critical issue blocks autonomous publication and routes the job to human review. |
category |
"accuracy" | "fluency" | "terminology" | "style" | "locale" |
Yes | What kind of problem it is. |
span |
string | null | No | The offending fragment of the translation, when the judge identified one. |
explanation |
string | Yes | What is wrong. |
suggestion |
string | null | No | Proposed replacement text. |
AgentStep, ToolTrace, AgentDecision
agent_run.steps[] is the workflow trace — one AgentStep per node the run executed. Read it for observability; do not branch product logic on step names, which are not part of the compatibility contract.
AgentStep:
| Field | Type | Default | Description |
|---|---|---|---|
id |
string | generated | 32-character hex. |
name |
string | — | Node name, e.g. quality_gate. |
actor |
string | — | Which agent ran it, e.g. quality_judge_agent. |
kind |
"agent" | "tool" | "human" | "system" |
agent |
What produced the step. |
status |
"pending" | "running" | "completed" | "skipped" | "failed" |
running |
Terminal steps read completed, skipped, or failed. |
input_summary |
string | null | null |
Human-readable summary of the input. |
output_summary |
string | null | null |
Human-readable summary of the result. |
tool_calls |
ToolTrace[] |
[] |
Tool invocations made inside this step. |
decisions |
AgentDecision[] |
[] |
Named decisions recorded by the step. |
error |
string | null | null |
Set when status is failed. |
started_at |
string (date-time) | generated | UTC. |
completed_at |
string (date-time) | null | null |
UTC. |
metadata |
object | {} |
Operator only. Free-form; absent in the tenant shape. |
ToolTrace: id, tool_name, status (same five values as AgentStep.status, default completed), input_summary, output_summary, error, started_at, completed_at, plus metadata (operator only).
AgentDecision: id, label, outcome, rationale (string | null), confidence (number 0–1 | null), created_at, plus metadata (operator only). The fast-path outcomes named elsewhere on this page — glossary_exact_held, tm_exact_noncompliant, tm_exact_held — appear here as outcome values.
Request metadata
metadata is an open object, but the pipeline reads a specific set of keys. Anything else round-trips untouched.
| Key | Type | Effect |
|---|---|---|
min_quality_score |
number | Per-request quality threshold, clamped to [0, 100]. Overrides the service threshold of 85 for both the repair decision and the autonomous-publish decision. 0 is a valid value and means "publish on any non-negative score". |
max_quality_loop_retries |
integer | Maximum automatic repair attempts, clamped to [1, 5]. Defaults to 2. |
candidate_models |
string (comma-separated) or array of strings | Opts into the multi-candidate path: extra candidate translations are produced alongside the baseline, scored against each other, and the best of them goes on to the quality gate. At most two extra candidates are used, and entries beyond that are dropped — the baseline the pipeline always produces occupies the remaining slot. Treat it as an opt-in switch rather than a selector: the values are not checked against any published list, and a request whose entries are all unusable is simply served on the baseline alone. Omit the key and no candidate or comparison work runs at all. The tenant shape does not echo the key back in the returned request.metadata. |
debate_rounds |
integer | How many rounds the candidates are compared over, clamped to [1, 3]. Defaults to 1. Only meaningful together with candidate_models. |
sensitive |
any truthy | Forces the job to human review: the deterministic glossary/TM fast paths will not publish it verbatim, and the review gate adds a sensitive_content trigger. |
regulated |
any truthy | Same as sensitive. |
requires_human_review |
any truthy | Same as sensitive. |
source_document |
object | Routes the request into the per-page flatten-to-text document flow instead of returning a single job. Set automatically by the upload paths; also honored on a JSON body. name is used to build golden download filenames. |
The same forced-review behavior is triggered by domain values finance, financial, healthcare, legal, medical, pharma.
Keys the pipeline writes back into request.metadata on the returned job — treat them as read-only:
| Key | Written when |
|---|---|
tenant |
Always, for an authenticated tenant: {"id": …, "name": …}. A client-supplied value is overwritten. |
primary_model |
An upload path (multipart or raw body) supplied candidate_models or model. Holds the first entry of the resulting roster. Not written on the JSON shape. Like candidate_models, the tenant shape does not echo this key back in the returned request.metadata. |
source_document |
An upload path produced the request: {"name": …, "content_type": …, "size_bytes": …, "page_number": …, "page_count": …} — the last two place the page among the document's pages. |
terminology_matches, terminology_entries |
Glossary terms matched the source text. |
terminology_gate |
Every quality-gate evaluation and every approve/modify review: passed, missing_terms, expected_target_counts, observed_target_counts, matched_source_terms. |
terminology_gate_override |
A reviewer published non-compliant output. |
golden_uri |
The golden store persist succeeded. |
provenance |
A deterministic fast path produced the translation: {"fast_path": "glossary_exact"} or {"fast_path": "tm_exact"}. |
review_pipeline_candidate |
A reviewer's first save: the pipeline's own output, kept so a later approve can tell whether the text it publishes is a reviewer's (human_edited). A value sent on a submission is discarded. |
Supported languages
source_lang and target_lang are validated against a fixed allowlist at the API boundary and again on the model. Matching is case-insensitive on the exact code (EN → en, zh-hans → zh-Hans). A code that is not in the list is rejected by design, regional tags and bare variant bases included — en-US, fr-FR, bare zh, and bare pt all fail. fr is French as written in France and fr-CA is Canadian French.
en, es, fr, fr-CA, de, it, pt-BR, pt-PT, nl, zh-Hans, zh-Hant, ja, ko, ar, ru, hi, tr, vi, th, id, pl, uk, cs, ro, hu, el, sv, da, nb, fi, ms, fa, ur, mr, ne
Batch submission
A JSON body containing inputs is parsed as a batch. One TranslationRequest is created per (input × target) pair, each is submitted through the full pipeline, and the endpoint responds with a JSON array of TranslationJob objects in submission order. The batch request body is not part of the generated OpenAPI schema — the endpoint accepts an untyped body and dispatches on its contents, so inputs and everything under it is documented here and nowhere else. The batch response is declared: the 200 schema for POST /api/translations is anyOf a single job and an array of jobs, so a generated client already has the array case.
| Field | Type | Required | Description |
|---|---|---|---|
inputs |
array | Yes | Batch items. |
inputs[].Text |
string | Yes | Source text. Each item is bounded by MAX_SOURCE_CHARS. A blank item rejects the whole request with 400 inputs[<n>].Text has no translatable content. |
inputs[].Language |
string | No | Source language, default "en". |
inputs[].TextType |
"Plain" | "Html" |
No | Default "Plain". |
inputs[].Targets |
array | Yes | One or more targets. Each target produces one job. |
inputs[].Targets[].Language |
string | Yes | Target language code. |
inputs[].Targets[].DeploymentName |
string | null | No | Inert — accepted and ignored. |
inputs[].Targets[].AllowFallback |
boolean | null | No | Inert. |
inputs[].Targets[].Tone |
string | null | No | Register instruction, per target. |
inputs[].Targets[].Gender |
string | null | No | Grammatical gender for agreement, per target. |
inputs[].Targets[].AdaptiveDatasetId |
string | null | No | Inert. |
These map onto the identically named TranslationRequest fields and carry the same functional/inert split — TextType, Tone and Gender steer the baseline; the other three are accepted for wire compatibility and ignored.
Batch bodies do not carry domain or metadata; per-request metadata knobs (min_quality_score, candidate_models, …) are only available on the single-text shape.
curl -X POST "https://trueidiom.com/api/translations?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: release-notes-2026-07" \
-d '{
"inputs": [
{
"Text": "Two-factor authentication is now required for all admins.",
"Language": "en",
"TextType": "Plain",
"Targets": [
{ "Language": "fr" },
{ "Language": "de" }
]
},
{
"Text": "Export your invoices from the billing page.",
"Language": "en",
"Targets": [{ "Language": "ja" }]
}
]
}'
import os
import requests
batch = {
"inputs": [
{
"Text": "Two-factor authentication is now required for all admins.",
"Language": "en",
"TextType": "Plain",
"Targets": [{"Language": "fr"}, {"Language": "de"}],
},
{"Text": "Export your invoices from the billing page.",
"Language": "en", "Targets": [{"Language": "ja"}]},
]
}
resp = requests.post(
"https://trueidiom.com/api/translations?api-version=2026-09-01",
headers={"X-API-Key": os.environ["TRUEIDIOM_API_KEY"],
"Idempotency-Key": "release-notes-2026-07"},
json=batch,
timeout=300,
)
resp.raise_for_status()
for job in resp.json():
print(job["id"], job["request"]["target_lang"])
// Node 18+
const batch = {
inputs: [
{ Text: "Two-factor authentication is now required for all admins.",
Language: "en", TextType: "Plain",
Targets: [{ Language: "fr" }, { Language: "de" }] },
{ Text: "Export your invoices from the billing page.", Language: "en",
Targets: [{ Language: "ja" }] },
],
};
const res = await fetch("https://trueidiom.com/api/translations?api-version=2026-09-01", {
method: "POST",
headers: {
"X-API-Key": process.env.TRUEIDIOM_API_KEY,
"Content-Type": "application/json",
"Idempotency-Key": "release-notes-2026-07",
},
body: JSON.stringify(batch),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
for (const job of await res.json()) {
console.log(job.id, job.request.target_lang);
}
The example creates three jobs. Language validation for batch payloads happens when the batch expands into individual requests: an unsupported code returns 422 with the underlying validation error text.
With an Idempotency-Key header, each expanded request gets <header value>:<index> (release-notes-2026-07:0, :1, :2) so a retried batch collapses onto the same three jobs.
The expanded requests run in order, and each is billed as it completes. If one fails, the request returns that item's error: the items before it are kept, billed and readable through GET /api/translations, and the items after it never run. A retry with the same key returns the finished items unchanged and runs the rest, re-running the one that failed.
Idempotency
Set Idempotency-Key: <value> on POST /api/translations, or send idempotency_key in the body (the header only fills in a body field that is absent). Keys are scoped to your tenant: the same value sent by another tenant is a different key and never returns your job. A submission whose key already belongs to one of your jobs returns that job as it stands now — still pending while the original request is running, or further along, such as human_edited after a review — with no new pipeline work and nothing further metered or billed. Two cases re-run the job instead, under the same id: a job that ended failed, and a job left pending for 10 minutes or more because the process running it stopped. Concurrent retries run it once, and it is billed once, when it completes. Keys are enforced atomically, across app instances.
On the batch shape the header is expanded per translation (Batch submission); on a text document, page n is keyed <value>:n, counting from 1 — the header's value, or the body's idempotency_key on the JSON metadata.source_document shape.
A replay that starts no work is answered before the gates. When every job the request names already exists and none of them ended failed or was left stale pending, the stored jobs are returned before the subscription/trial gate, the character-budget gates and the storage-binding check — so a retry is never refused with 402, 429 or 409 for work that has already happened (the retry of a request that took the month's last characters gets its job back). Nothing is metered again. A replay that has to re-run a job — a failed one, or a stale pending one — is new work and passes every gate first, like any submission; refused there, nothing is metered and the stored job is untouched. Malformed bodies and unsupported languages are reported before either.
Limits
These are the documented constraints on a submission.
| Constraint | Limit | Enforced as |
|---|---|---|
| Characters per source text | 50000 |
413 source text exceeds MAX_SOURCE_CHARS. Checked on source_text and on every inputs[].Text. Sized so a request completes inside the documented timeout — this endpoint runs the pipeline inline. |
| Translations per batch | 50 |
413 batch exceeds MAX_BATCH_ITEMS. Counted as the total number of (input, target) pairs; a single-text submission counts as 1. Also caps the page count of a flatten-to-text document upload. |
| Declared request size | 10000000 bytes |
413 request body exceeds MAX_REQUEST_BYTES, checked against Content-Length — before the body is read on a JSON or raw-body submission, and on a flatten-to-text multipart upload once the form shows it is not a PDF, DOCX or XLSX. |
| Absolute request size | 30000000 bytes |
413 request body exceeds MAX_UPLOAD_BYTES, enforced app-wide on the streamed body. The only size bound on a PDF, DOCX or XLSX uploaded to this endpoint, as on POST /api/documents/translate. |
| Characters per document page | 5000 |
Flatten-to-text uploads are split at this size, one job per page. |
| Monthly source characters per tenant | 500000 (monthly_char_limit, per account) |
429 tenant usage limit exceeded, counted from the incoming payload before any work starts and weighted per language pair (see below), together with the tenant's other work still in flight (text requests and document jobs); the detail names the tenant's remedies (Quotas). |
| Free-trial source characters | 150000 |
With billing in force, a 402 once the one-time grant is exhausted. Drawn down on the same weighted count as the monthly allowance. Text translation only — layout document translation is never trial-covered. |
| Free-trial length in days | 14 |
With billing in force, a 402 once this many days have elapsed since account creation, even with characters left on the grant. |
Dense-script weighting. The two character budgets above are not raw counts. On a language pair where either side is zh-Hans, zh-Hant, ja, ko, th, hi, mr or ne, each source character counts as 3.5 by default; every other pair counts 1:1. That weighted number is what draws down the monthly allowance and the trial grant, what your recorded usage shows, and what the 429 and 402 gates evaluate before any work starts — so a 10,000-character English-to-Japanese submission is admitted, metered and reported as 35,000. A batch weighs each (input, target) pair on its own — that input's Language against that target — so one submission can mix weighted and unweighted pairs. The MAX_SOURCE_CHARS ceiling in the first row is the exception: it bounds the raw length of the text you send, unweighted. Tenants & billing carries the full treatment.
Quality score
Every non-fast-path job is scored 0–100 for translation quality. The score you read on the job is quality_assessment.score; the score the gate acted on is agent_run.quality_gate.score, and the bar it was compared against is agent_run.quality_gate.threshold.
How the gate decides.
- The baseline is scored once, together with a deterministic glossary-compliance check.
- Pass — score ≥ threshold, terminology compliant, no
criticalissue — the translation ships as-is. No repair, no verification. - Fail — the translation is automatically repaired against the assessment and the tenant's terminology and translation-memory guidance, then scored again. This repeats up to the retry limit, and the best-scoring candidate wins, so a repair can never ship a regression.
refinementis populated. - A candidate that went through repair is checked once more before the review gate runs.
verificationis populated.
Autonomous publish (quality_gate.decision == "pass", job goes straight to published) requires: score ≥ threshold, terminology compliant, no critical issue, no blocking trigger, and the request not marked sensitive/regulated. If a repair happened, it additionally requires verification.passed, verification.score ≥ threshold, and that the repaired text did not diverge drastically from the text it repaired (the baseline, or the chosen candidate on the multi-candidate path). Otherwise the job lands in awaiting_review with a HumanReviewTask explaining why.
What a caller can set per request.
| Knob | Where | Range | Default |
|---|---|---|---|
| Threshold | metadata.min_quality_score |
0–100 (clamped) |
85 |
| Repair attempts | metadata.max_quality_loop_retries |
1–5 (clamped) |
2 |
| Extra candidates | metadata.candidate_models |
Up to two extra candidates | none (single-candidate path) |
| Comparison rounds | metadata.debate_rounds |
1–3 (clamped) |
1 |
| Force human review | metadata.sensitive / regulated / requires_human_review, or a sensitive domain |
— | off |
Raising min_quality_score makes autonomous publication less likely and human review more likely; lowering it does the opposite.
Two deterministic fast paths skip scoring entirely. When the whole source segment is itself a glossary entry (fast_path: "glossary_exact"), or when a 100% translation-memory match is terminology-compliant (fast_path: "tm_exact"), the stored target is returned with no new translation work — verbatim, except that a glossary target follows the segment's casing unless its entry is case-sensitive (an ALL-CAPS segment gets an ALL-CAPS target, and a capitalised segment capitalises a target stored in lowercase), and the whole-segment glossary comparison counts accents and tone marks: status is published, quality_gate.score is 100.0, metadata.provenance.fast_path records which path fired, and nothing is written to the golden store or TM (the pair already lives there). A request marked sensitive/regulated never takes a fast path — it falls through to the gated pipeline and human review.
End-to-end example
1. Submit
curl -X POST "https://trueidiom.com/api/translations?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: kb-article-338-fr" \
-d '{
"source_text": "Contact support before disabling two-factor authentication.",
"source_lang": "en",
"target_lang": "fr",
"domain": "software",
"metadata": { "min_quality_score": 92 }
}'
The response is the completed job. Read status and agent_run.quality_gate.decision:
{
"id": "e05b7d1c93af4266b8d5c0a17e42f9b3",
"status": "awaiting_review",
"baseline_translation": "Contactez l'assistance avant de désactiver l'authentification à deux facteurs.",
"final_translation": "Contactez le support avant de désactiver l'authentification à deux facteurs.",
"agent_run": {
"status": "waiting_for_human",
"quality_gate": {
"decision": "needs_human",
"score": 88.0,
"threshold": 92.0,
"reasons": ["Quality score is below the review threshold."],
"triggers": [
{
"code": "low_quality_score",
"severity": "warning",
"message": "Quality score is below the review threshold.",
"evidence": { "score": 88.0, "threshold": 92.0 }
}
]
},
"human_review_task": {
"status": "open",
"reason": "Human review requested: 1 warning trigger(s).",
"question": "Should this fr translation be approved as-is, edited before publishing, or rejected?",
"requested_actions": ["save", "approve", "modify", "reject"]
}
}
}
The low_quality_score trigger here is warning severity, and a warning does not by itself force review — what sent this job to a human is the score sitting below the threshold the request asked for. A gate that published autonomously can carry warning triggers and their reasons in exactly this shape, so read decision and status rather than testing whether the arrays are empty.
2. Re-read the job (any time, from any process)
curl "https://trueidiom.com/api/translations/e05b7d1c93af4266b8d5c0a17e42f9b3?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
Or pull the whole review queue:
curl -G "https://trueidiom.com/api/translations?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
--data-urlencode "status=awaiting_review"
3. Review
curl -X POST "https://trueidiom.com/api/translations/e05b7d1c93af4266b8d5c0a17e42f9b3/review?api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"action": "modify",
"reviewer": "jean.dupont@acme.example",
"final_translation": "Contactez l'\''assistance avant de désactiver l'\''authentification à deux facteurs.",
"notes": "House style uses \"assistance\", not \"support\"."
}'
The response comes back with "status": "human_edited", "review_action": "modify", a golden_uri in request.metadata, and a reviewer_edit entry in learning_signals.
4. Fetch the golden translation
curl "https://trueidiom.com/api/translations/e05b7d1c93af4266b8d5c0a17e42f9b3/golden?format=txt&api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
Contactez l'assistance avant de désactiver l'authentification à deux facteurs.
The same corrected pair is now in the tenant's golden translation memory, where later jobs reuse it verbatim on an exact match and draw on it for close ones. Export it with GET /api/tm/export/golden.
Error reference
The canonical error catalog — body shapes, the full error_code vocabulary, and status-code semantics — is in Conventions & errors. The table below lists only what these endpoints emit.
Standard HTTPException responses carry {"detail": "<message>"}. Errors raised inside the pipeline carry {"detail", "error_code", "request_id"}, where request_id is the same id as the X-Request-ID response header — the one you sent, or the generated one — and is never null.
| Status | error_code (when present) |
Where it comes from |
|---|---|---|
400 |
validation_error |
Malformed body, unusable Content-Length, blank source text, an upload that cannot be read, or a review action that is invalid for the job's state. |
401 |
— | Missing or invalid tenant credentials (the golden download included), or no admin credentials on DELETE. |
402 |
— | Subscription/trial gate on POST /api/translations when billing is in force. |
403 |
— | DELETE /api/translations/{job_id} by an authenticated non-admin. |
404 |
not_found |
Unknown job id, another tenant's job, an empty golden translation, or a job deleted while its pipeline or review was running. |
409 |
storage_binding_not_verified |
The tenant's own storage binding is not verified or cannot be used (a missing or unreadable secret, an account URL that no longer validates): a submission refused before any work (no job created), or a golden persist it blocked — on review, typically (the job is parked). |
409 |
— | Golden download requested for a job that is not published/human_edited. |
413 |
— | A payload exceeded one of the documented limits. |
415 |
— | Unsupported upload type. |
422 |
— | Request-model validation, including unsupported language codes and an invalid status or format value. |
422 |
content_filtered |
The model provider's content policy refused the text; retrying the same text is refused again. The job is saved with status: "failed". |
429 |
— | Tenant monthly source-character limit (tenant usage limit exceeded). |
429 |
rate_limit |
Upstream translation capacity saturated inside the pipeline; retry after a pause. |
429 |
quota_exceeded |
A layout-document upload with wait set whose month has no headroom left once the document's characters are counted. |
500 |
internal_error |
Any other failure inside the pipeline: {"detail": "internal error", "error_code": "internal_error", "request_id": "<id>"}. The job is saved with status: "failed" and agent_run.error internal error. |
502 |
persistence_failed |
Golden store write failed; the job is parked as persistence_failed. |
503 |
— | translation is temporarily disabled. |
503 |
transient_error |
A retryable upstream failure — the model provider was unavailable, or the service's own secret storage could not be read to check the tenant's storage binding. |
504 |
stage_timeout |
A layout-document upload with wait set whose document stage exceeded its time budget. |
What's next
- Authentication — get the credential every call on this page needs.
- Conventions & errors — the error catalog every endpoint here can return.
- Documents — the asynchronous pipeline for PDF, DOCX and XLSX files.
- Terminology & TM — the glossary and translation memory that steer every job, and return an exact repeat without re-running the pipeline.