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. The batch request body is absent from it entirely; 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 (admin-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. |
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}, and POST /api/translations/{job_id}/review 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; included in pipeline error bodies. Generated when absent. |
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 five 402 bodies. While billing is not in force there are no 402 responses and no metering. |
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. |
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 after a reviewer supplied a corrected final_translation (modify). 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. |
persistence_failed | The translation was accepted but writing it to the golden store failed — most often a Bring-Your-Own-Storage binding that is not verified. The job is parked and replays automatically once the binding verifies. |
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. |
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
}
}'
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_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. |
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. |
409 | workspace storage (BYOS) binding is not verified — … | Autonomous publish tried to persist to an unverified BYOS binding. error_code: storage_binding_not_verified. The job is parked as persistence_failed. |
413 | request body exceeds MAX_REQUEST_BYTES | Declared Content-Length above the limit. |
413 | request body exceeds MAX_UPLOAD_BYTES | Streamed body above the absolute app-wide ceiling. |
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 (including a missing target_lang). |
422 | HTTPValidationError body | Body failed model validation (e.g. source_text missing). |
429 | tenant usage limit exceeded | This request's source characters would push the tenant past its monthly_char_limit. The detail continues with the remedies that tenant has — a top-up for this period, a plan switch for the next (Quotas). |
502 | golden persistence failed: … — the job is parked as persistence_failed and can be retried | error_code: persistence_failed. |
503 | translation is temporarily disabled | Translation is temporarily unavailable. |
Pipeline failures surface through the shared error handler with {"detail", "error_code", "request_id"}: 429 rate_limit, 503 transient_error, 504 stage_timeout, 500 internal_error. A job whose pipeline raised is saved with status: "failed" and can still be read with GET /api/translations/{job_id}.
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) | 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. |
| 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, split into ≤5,000-character pages, and each page is submitted as its own text job. 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": "data/jobs/handbook-fr.txt"
}
output_file is a server-side path to the concatenated per-page output (pages separated by === Page N === markers), not a URL. To retrieve translated content over the API, read each id in job_ids with GET /api/translations/{job_id} or download it from GET /api/translations/{job_id}/golden.
413 document exceeds MAX_BATCH_ITEMS when split into pages is returned when the page count exceeds MAX_BATCH_ITEMS; 400 source_document has no translatable content when extraction yields nothing.
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"
[
{
"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"
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. |
422 | HTTPValidationError body | Malformed path parameter. |
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 pipeline's candidate (final_translation, else refinement.refined_translation, else baseline_translation): the candidate is published as-is → published. A differing value is a reviewer edit: it is applied and the job lands as human_edited, exactly as if modify had been used. (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 an edit was applied | 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). - 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), 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."
}'
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 (deployments with no tenant accounts, or a job deleted between the two lookups). |
409 | workspace storage (BYOS) binding is not verified — … | Golden persist blocked by an unverified storage binding. |
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: optional. This endpoint uses the optional identity resolver so that plain browser navigations from the review UI (which cannot send headers) work. When credentials are supplied, normal tenant scoping applies and another tenant's job returns 404.
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. Non-alphanumeric characters are replaced with _.
Example
curl -OJ "https://trueidiom.com/api/translations/9c2f41b0a7d84e5b8c1d3e6f0a2b4c8d/golden?format=docx&api-version=2026-09-01" \
-H "X-API-Key: $TRUEIDIOM_API_KEY"
Errors
| Status | detail | Cause |
|---|---|---|
404 | job not found | Unknown id, or (with credentials) 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 — the global_administrator role of the job's own tenant, or the platform operator API key. A non-admin member gets 403; an admin of another tenant gets 404 (deletion never confirms a foreign job id exists). Only the operator key 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.
After deletion, GET /api/translations/{job_id} and the public golden download link 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 $ACCESS_TOKEN"
Errors
| Status | detail | Cause |
|---|---|---|
401 | admin credentials required | No usable credentials. |
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_modelsechoed 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": …}. |
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"}. |
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). Regional subtags and bare variant bases are rejected by design — en-US, fr-CA, bare zh, and bare pt all fail.
en, es, fr, de, it, pt-BR, pt-PT, nl, zh-Hans, zh-Hant, ja, ko, ar, ru, hi, tr, vi, th, id
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. |
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" }]
}
]
}'
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.
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). A submission whose key already belongs to a job returns that existing job unchanged, with no new pipeline work and nothing further metered or billed. Keys are stored per job and enforced atomically — with the Postgres job store, by a partial unique index shared across app instances.
A replay is still admission-checked. The subscription/trial gate and the character-budget gates run on the way in, before the key is looked up, and they weigh the replayed payload as if it were new work. Close to your monthly allowance or your remaining trial balance, a replay can therefore come back 429 or 402 even though serving it from the stored job would have cost nothing — and a subscription that lapsed between the first call and the retry refuses the retry outright. Nothing is metered when that happens and the stored job is untouched: the same key returns it once there is headroom again.
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. |
| Absolute request size | 30000000 bytes | 413 request body exceeds MAX_UPLOAD_BYTES, enforced app-wide on the streamed body. |
| 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); 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 or hi, 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, so one submission can mix weighted and unweighted destinations. 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 baseline. 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 verbatim, with no new translation work: 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 echoes the X-Request-ID request header when one was sent.
| Status | error_code (when present) | Where it comes from |
|---|---|---|
400 | validation_error | Malformed body, unusable Content-Length, or a review action that is invalid for the job's state. |
401 | — | Missing or invalid tenant credentials, 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, or an empty golden translation. |
409 | storage_binding_not_verified | Golden persist blocked by an unverified BYOS binding. |
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. |
429 | rate_limit | Tenant monthly source-character limit, or an upstream rate limit hit inside the pipeline. |
500 | internal_error | Unhandled pipeline failure. The job is saved with status: "failed". |
502 | persistence_failed | Golden store write failed; the job is parked as persistence_failed. |
503 | transient_error | Translation temporarily unavailable, or a retryable upstream failure. |
504 | stage_timeout | A pipeline stage exceeded its timeout budget. |