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; the DELETE response 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 declared application/json with 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:

CredentialHeaderWhere you get it
Tenant API keyX-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 tokenAuthorization: Bearer <access_token>POST /api/oauth2/token — see the token endpoint. Resolves the caller's tenant.
Tenant selectorX-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 /billing first, or drive the examples with an OAuth2 access token from POST /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.

HeaderApplies toEffect
X-Api-VersionallAlias 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-KeyPOST /api/translationsDeduplicates submissions. See Idempotency.
X-Request-IDallEchoed back on every response; included in pipeline error bodies. Generated when absent.
X-Source-Filenameraw-body uploadsNames 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.

ConditionCaller-visible behavior
Translation is temporarily unavailablePOST /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 forcePOST /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

ValueMeaning
pendingJob created; the workflow has not produced a baseline yet. Initial value of TranslationJob.status.
translatedA baseline translation exists (baseline_translation is populated).
refinedPresent 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_reviewThe 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.
approvedTransient: 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_editedPublished after a reviewer supplied a corrected final_translation (modify). Golden download is available.
rejectedA reviewer rejected the translation. Nothing is persisted to the golden store or TM.
publishedFinal translation accepted — autonomously by the quality gate, via a reviewer approve, or via a deterministic glossary/TM fast path. Golden download is available.
failedThe pipeline raised. The error is also on agent_run.error / agent_run.status.
persistence_failedThe 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.

NameTypeRequiredDescription
source_textstringYesText to translate. Length is bounded by MAX_SOURCE_CHARS.
target_langstringYesTarget language code. Must be in the supported set. Case-insensitive; normalized to the canonical code (ESes).
source_langstringNoSource language code. Default "en". Same allowlist and normalization.
idempotency_keystring | nullNoDeduplication key. The Idempotency-Key header fills this in when the field is absent.
domainstring | nullNoDomain hint that steers the translation and its quality assessment. The values finance, financial, healthcare, legal, medical, pharma force the job to human review.
metadataobjectNoFree-form. Recognized keys are listed under Request metadata; unknown keys are stored and returned untouched. Defaults to {}.
text_type"Plain" | "Html" | nullNo"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_namestring | nullNoInert. Accepted and returned unchanged; ignored.
adaptive_dataset_idstring | nullNoInert. Accepted and returned unchanged; ignored.
allow_fallbackboolean | nullNoInert. Accepted and returned unchanged; ignored.
tonestring | nullNoRegister 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.
genderstring | nullNoGrammatical 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:

  • Functionaltext_type, tone and gender steer 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.
  • Inertdeployment_name, adaptive_dataset_id and allow_fallback have no equivalent and are ignored. They are still accepted, stored on job.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, with metadata.candidate_models as 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

StatusdetailCause
400invalid Content-Length headerContent-Length is not an integer.
400invalid JSON body: …Body did not parse as JSON.
400JSON body must be an objectBody parsed to a non-object (array, string, number).
400source_document upload is requiredA 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.
401missing or invalid tenant credentials / missing or invalid API keySee Authentication.
402an active subscription is required to run translations — add a payment method at /billingBilling is in force and the tenant has no current subscription and no trial grant.
402the free trial covers the workspace only — API access requires an active subscription; subscribe at /billingThe 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.
402your free trial is used up — add a payment method at /billing to continueTrial grant exhausted. Checked before the clock, so a spent grant always reads this way.
402your free trial has ended — add a payment method at /billing to continueThe trial's calendar clock (14 days from account creation) ran out with characters still on the grant.
402this request exceeds your remaining free trial characters — add a payment method at /billingThis request's billable characters exceed the remaining trial balance. Counted before any work starts, and weighted — see Limits.
409workspace 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.
413request body exceeds MAX_REQUEST_BYTESDeclared Content-Length above the limit.
413request body exceeds MAX_UPLOAD_BYTESStreamed body above the absolute app-wide ceiling.
413batch exceeds MAX_BATCH_ITEMSSee Limits.
413source text exceeds MAX_SOURCE_CHARSSee Limits.
415unsupported source_document typeUpload mode with an unsupported file type.
422unsupported language(s): 'xx'. Supported: en, es, fr, …source_lang or target_lang outside the allowlist (including a missing target_lang).
422HTTPValidationError bodyBody failed model validation (e.g. source_text missing).
429tenant usage limit exceededThis 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).
502golden persistence failed: … — the job is parked as persistence_failed and can be retriederror_code: persistence_failed.
503translation is temporarily disabledTranslation 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:

ShapeTriggerResult
BatchJSON body containing inputsArray of TranslationJob. See Batch submission.
Layout documentmultipart/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 typeText 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 typeSame per-page flow; every parameter is a query parameter.
JSON with metadata.source_documentJSON body where metadata.source_document is an objectSame per-page flatten-to-text flow as above.

Layout-document form fields

NameTypeRequiredDescription
source_documentfileYesPDF, DOCX or XLSX.
target_langstringYesTarget language code.
source_langstringNoDefault en.
domainstringNoDomain hint.
tonestringNoRegister instruction applied to every segment's baseline translation. Blank or absent means no register instruction, not a default.
genderstringNoGrammatical gender for agreement, applied to every segment's baseline translation.
text_typestringNoAccepted 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/Html400. See POST /api/documents/translate.
waitstringNo1, 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)

NameTypeRequiredDescription
source_documentfileYesText-bearing file. Zero bytes → 400 source_document is empty.
target_langstringYesTarget 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_langstringNoDefault en.
domainstringNoDomain hint.
text_typestringNoDefault Plain. Html applies the HTML-fragment handling described under TranslationRequest.
tonestringNoRegister instruction applied to every page's baseline translation. Blank or absent means no register instruction, not a default.
genderstringNoGrammatical gender for agreement, applied to every page's baseline translation.
deployment_namestringNoInert — 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_idstringNoInert. Same grouping rule as deployment_name.
allow_fallbackboolean-ish stringNoInert. Same grouping rule as deployment_name.
candidate_modelsstringNoComma-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.
modelstringNoSingle-value alternative to candidate_models, merged into the same list.
min_quality_scorenumberNoPopulates 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.

NameTypeRequiredDescription
target_langstringYesBlank or absent → 400 target_lang query parameter is required for text uploads.
source_langstringNoDefault en.
domainstringNoDomain hint.
text_typestringNoDefault Plain.
tonestringNoRegister instruction applied to every page's baseline translation. Parity with the multipart form.
genderstringNoGrammatical gender for agreement. Parity with the multipart form.
deployment_namestringNoInert — accepted and ignored. Same grouping rule as the multipart form.
adaptive_dataset_idstringNoInert.
allow_fallbackboolean-ish stringNoInert.
candidate_modelsstringNoComma-separated → metadata.candidate_models and metadata.primary_model. Same extra-candidate opt-in as the multipart form.
modelstringNoSingle-value alternative to candidate_models.
min_quality_scorenumberNometadata.min_quality_score.
filenamestringNoNames 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

NameTypeRequiredDescription
statusJobStatus | nullNoFilter 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

StatusCause
401Missing or invalid credentials.
422status 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

NameTypeRequiredDescription
job_idstringYesThe 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

StatusdetailCause
401missing or invalid tenant credentialsNo usable credentials.
404job not foundUnknown id, or a job owned by another tenant.
422HTTPValidationError bodyMalformed 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

NameTypeRequiredDescription
job_idstringYesJob to review. Must be in status awaiting_review or refined.

Request body — ReviewSubmission

NameTypeRequiredDescription
action"save" | "approve" | "modify" | "reject"YesThe reviewer decision. See the table below.
reviewerstringYesRequired. 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_translationstring | nullConditionalRequired 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.
notesstring | nullNoFree text stored on job.review_notes and attached to the review decision.

Action semantics

actionfinal_translationResulting statusGolden store / TM / learning
saveOptional — stored as a draft; when omitted the current best candidate is keptawaiting_review (unchanged)Not run. The review task stays open.
approveOptional. 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.)approvedpublished, or human_edited when an edit was appliedPersisted, TM dual-write, learning signals emitted.
modifyRequiredapprovedhuman_editedPersisted, TM dual-write, learning signals emitted (including a reviewer_edit signal capturing the before/after).
rejectIgnoredrejectedNot run. The review task is closed.

What happens on approve/modify

  1. job.reviewer, job.review_action, and job.review_notes are recorded, and the open HumanReviewTask is closed with the reviewer's name.
  2. 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 at request.metadata.terminology_gate_override ({"overridden": true, "by": …, "action": …, "missing_terms": {…}}).
  3. 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 as persistence_failed and the request returns 502 (or 409 for an unverified BYOS binding).
  4. 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 returns 200. A correction to a source already in memory is admitted even at the cap. See Terminology & translation memory. The golden TM set is readable through GET /api/tm/export/golden.
  5. 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.
  6. The status advances to published (approve) or human_edited (modify), and agent_run.status becomes completed.

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

Statusdetail / error_codeCause
400`final_translation` is required for `modify`error_code: validation_errormodify without a replacement translation.
400Job not reviewable in status JobStatus.PUBLISHEDerror_code: validation_errorThe job is not in awaiting_review or refined.
401missing or invalid tenant credentialsNo usable credentials.
404job not foundUnknown id, or a job owned by another tenant.
404translation job … not founderror_code: not_foundRaised 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).
409workspace storage (BYOS) binding is not verified — …Golden persist blocked by an unverified storage binding.
422HTTPValidationError bodyaction is not one of the four values, or reviewer is missing.
502golden 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

NameTypeRequiredDescription
job_idstringYesJob to download. Must be in status published or human_edited.

Query parameters

NameTypeRequiredDescription
formatstringNotxt (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.

formatContent-TypeFilename
txttext/plain; charset=utf-8<stem>.txt
docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document<stem>.docx
pdfapplication/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

StatusdetailCause
404job not foundUnknown id, or (with credentials) another tenant's job.
404golden translation is emptyThe job has no final_translation.
409golden translation is not available for this job statusThe job is not published or human_edited — including approved, awaiting_review, rejected, and persistence_failed.
422HTTPValidationError bodyformat 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

NameTypeRequiredDescription
job_idstringYesJob 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

StatusdetailCause
401admin credentials requiredNo usable credentials.
403admin role requiredAuthenticated, but not a global_administrator.
404job not foundUnknown 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.

FieldTypeDescription
idstringJob id (32-character hex). Server-generated.
requestTranslationRequestThe submitted request, plus metadata the pipeline wrote back. Always present.
agent_runAgentRunFull 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_translationstring | nullThe first-pass translation, before any repair.
quality_assessmentQualityAssessment | nullscore (0–100), issues[], reasoning, created_at, plus model and usage (operator only).
refinementRefinement | nullPopulated only when the job was automatically repaired: score, refined_translation, issues[], reasoning, created_at, plus model and usage (operator only).
verificationVerificationResult | nullPopulated only when a repair happened: passed, score, issues[], checks ({name: bool}), risk_flags[], reasoning, created_at, plus model and usage (operator only).
final_translationstring | nullThe translation to ship. Set by the quality gate, by a reviewer, or by a glossary/TM fast path.
reviewerstring | nullIdentity recorded by the review submission. null until a review happens.
review_action"save" | "approve" | "modify" | "reject" | nullLast review action.
review_notesstring | nullReviewer notes.
learning_signalsLearningSignal[]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.
statusJobStatusSee Job lifecycle. Default pending.
created_atstring (date-time)UTC, e.g. 2026-07-26T14:03:07.220188Z.
updated_atstring (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 model and no usage on quality_assessment, refinement or verification; their score, issues[], reasoning, created_at — and refined_translation, passed, checks, risk_flags — are unchanged;
  • no metadata object anywhere under agent_run — not on the run, its steps[], their tool_calls[], or their decisions[] — while the narrative those carry (names, statuses, input/output summaries, decision rationale, timestamps) stays;
  • agent_run.quality_gate.metadata keeps assessment_score, verification_score, verification_passed, risk_flags, trigger_codes, terminology_gate, repaired and fast_path, but not the operator-only execution trace;
  • no candidate_models echoed back in request.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) — decision is pass, needs_human, or fail; score and threshold are the numbers the decision was made on; triggers[] lists everything the gate raised and reasons[] 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: a pass gate can carry a non-empty reasons[] and triggers[] — only a blocking trigger forces review, while a warning one (terminology_uncertainty, risk_flag) rides along on a job that publishes autonomously, and a fast-path pass carries a one-line reasons[] with triggers[] empty. Branch on decision and on status, never on whether these arrays are empty. metadata carries assessment_score, verification_score, verification_passed, risk_flags, trigger_codes, terminology_gate, repaired, fast_path when 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

FieldTypeDefaultDescription
idstringgenerated32-character hex task id.
status"open" | "completed" | "cancelled"opencompleted after POST /api/translations/{job_id}/review closes it.
requested_bystringreview_gate_agentWhich component opened the task.
reasonstringRequired. Display-ready summary, e.g. Human review requested: 1 warning trigger(s).
questionstring | nullnullDisplay-ready question for the reviewer.
instructionsstring | nullnullOptional extra guidance for the reviewer.
requested_actionsarray of "save" | "approve" | "modify" | "reject"all fourThe actions the review endpoint will accept for this task.
triggersHumanReviewTrigger[][]Why review was requested.
completed_bystring | nullnullThe reviewer value from the review submission.
created_atstring (date-time)generatedUTC.
completed_atstring (date-time) | nullnullSet when the task closes.
metadataobject{}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".

FieldTypeRequiredDescription
severity"minor" | "major" | "critical"YesA single critical issue blocks autonomous publication and routes the job to human review.
category"accuracy" | "fluency" | "terminology" | "style" | "locale"YesWhat kind of problem it is.
spanstring | nullNoThe offending fragment of the translation, when the judge identified one.
explanationstringYesWhat is wrong.
suggestionstring | nullNoProposed 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:

FieldTypeDefaultDescription
idstringgenerated32-character hex.
namestringNode name, e.g. quality_gate.
actorstringWhich agent ran it, e.g. quality_judge_agent.
kind"agent" | "tool" | "human" | "system"agentWhat produced the step.
status"pending" | "running" | "completed" | "skipped" | "failed"runningTerminal steps read completed, skipped, or failed.
input_summarystring | nullnullHuman-readable summary of the input.
output_summarystring | nullnullHuman-readable summary of the result.
tool_callsToolTrace[][]Tool invocations made inside this step.
decisionsAgentDecision[][]Named decisions recorded by the step.
errorstring | nullnullSet when status is failed.
started_atstring (date-time)generatedUTC.
completed_atstring (date-time) | nullnullUTC.
metadataobject{}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 01 | 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.

KeyTypeEffect
min_quality_scorenumberPer-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_retriesintegerMaximum automatic repair attempts, clamped to [1, 5]. Defaults to 2.
candidate_modelsstring (comma-separated) or array of stringsOpts 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_roundsintegerHow many rounds the candidates are compared over, clamped to [1, 3]. Defaults to 1. Only meaningful together with candidate_models.
sensitiveany truthyForces 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.
regulatedany truthySame as sensitive.
requires_human_reviewany truthySame as sensitive.
source_documentobjectRoutes 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:

KeyWritten when
tenantAlways, for an authenticated tenant: {"id": …, "name": …}. A client-supplied value is overwritten.
primary_modelAn 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_documentAn upload path produced the request: {"name": …, "content_type": …, "size_bytes": …}.
terminology_matches, terminology_entriesGlossary terms matched the source text.
terminology_gateEvery quality-gate evaluation and every approve/modify review: passed, missing_terms, expected_target_counts, observed_target_counts, matched_source_terms.
terminology_gate_overrideA reviewer published non-compliant output.
golden_uriThe golden store persist succeeded.
provenanceA 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 (ENen, zh-hanszh-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.

FieldTypeRequiredDescription
inputsarrayYesBatch items.
inputs[].TextstringYesSource text. Each item is bounded by MAX_SOURCE_CHARS.
inputs[].LanguagestringNoSource language, default "en".
inputs[].TextType"Plain" | "Html"NoDefault "Plain".
inputs[].TargetsarrayYesOne or more targets. Each target produces one job.
inputs[].Targets[].LanguagestringYesTarget language code.
inputs[].Targets[].DeploymentNamestring | nullNoInert — accepted and ignored.
inputs[].Targets[].AllowFallbackboolean | nullNoInert.
inputs[].Targets[].Tonestring | nullNoRegister instruction, per target.
inputs[].Targets[].Genderstring | nullNoGrammatical gender for agreement, per target.
inputs[].Targets[].AdaptiveDatasetIdstring | nullNoInert.

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.

ConstraintLimitEnforced as
Characters per source text50000413 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 batch50413 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 size10000000 bytes413 request body exceeds MAX_REQUEST_BYTES, checked against Content-Length before the body is read.
Absolute request size30000000 bytes413 request body exceeds MAX_UPLOAD_BYTES, enforced app-wide on the streamed body.
Characters per document page5000Flatten-to-text uploads are split at this size, one job per page.
Monthly source characters per tenant500000 (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 characters150000With 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 days14With 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.

  1. The baseline is scored once, together with a deterministic glossary-compliance check.
  2. Pass — score ≥ threshold, terminology compliant, no critical issue — the translation ships as-is. No repair, no verification.
  3. 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. refinement is populated.
  4. A candidate that went through repair is checked once more before the review gate runs. verification is 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.

KnobWhereRangeDefault
Thresholdmetadata.min_quality_score0100 (clamped)85
Repair attemptsmetadata.max_quality_loop_retries15 (clamped)2
Extra candidatesmetadata.candidate_modelsUp to two extra candidatesnone (single-candidate path)
Comparison roundsmetadata.debate_rounds13 (clamped)1
Force human reviewmetadata.sensitive / regulated / requires_human_review, or a sensitive domainoff

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.

Statuserror_code (when present)Where it comes from
400validation_errorMalformed body, unusable Content-Length, or a review action that is invalid for the job's state.
401Missing or invalid tenant credentials, or no admin credentials on DELETE.
402Subscription/trial gate on POST /api/translations when billing is in force.
403DELETE /api/translations/{job_id} by an authenticated non-admin.
404not_foundUnknown job id, another tenant's job, or an empty golden translation.
409storage_binding_not_verifiedGolden persist blocked by an unverified BYOS binding.
409Golden download requested for a job that is not published/human_edited.
413A payload exceeded one of the documented limits.
415Unsupported upload type.
422Request-model validation, including unsupported language codes and an invalid status or format value.
429rate_limitTenant monthly source-character limit, or an upstream rate limit hit inside the pipeline.
500internal_errorUnhandled pipeline failure. The job is saved with status: "failed".
502persistence_failedGolden store write failed; the job is parked as persistence_failed.
503transient_errorTranslation temporarily unavailable, or a retryable upstream failure.
504stage_timeoutA pipeline stage exceeded its timeout budget.