Technical Documents Translated Perfectly — Code, Diagrams & All
"We use Amazon Bedrock to do what generic translation services can't."
Technical documentation is hard to translate. Generic translation services — including AWS Translate — don't understand the boundary between human language and machine language. They translate code blocks, corrupt variable names, localize version numbers, and hallucinate brand names.
We learned this the hard way. AWS Translate cost us over $800 in worthless translations before we caught it corrupting technical content across 11 languages. Silent failures. No warnings. No way to know without reading every translated document.
Real failure modes we caught:
function getName() → función obtenerNombre() (code no longer executes)PostgreSQL 17.7 → PostgreSQL (version number dropped)32 GB → 32 Go (unit localized in French)So we built our own translation engine. It runs on Amazon Bedrock with sentinel preprocessing and multi-layer validation. Before an AI agent ever sees your document, the engine lifts out everything that must not change — code, diagrams, version numbers, brand names, technical identifiers — and puts it back untouched afterwards. The agent cannot corrupt what it never touches, and every translated section is checked before it is delivered to you.
Six AI agents. Same protection. A price you see before you pay.
You pick the agent that suits your document. Five of the six cost you the same flat $3.00 per document-language pair whatever the size, and Claude Opus 4.8 starts at $3.00 and follows its real cost on larger documents, so the choice is about the kind of translation you want, never a surprise on the bill. The engine is agent-agnostic by design: any model reachable through Amazon Bedrock can serve as a translation agent, and these six were chosen for quality and for their distinct strengths.
Your default is Auto — best agent per language. Leave the agent out of your quote and Auto picks for you: Qwen 3 235B for every language Qwen supports, and Claude Sonnet 4.6 only for the five languages outside Qwen's published list (Amharic, Hausa, Mongolian, Pashto and Somali), at the same flat price. Choose an agent yourself and that choice overrides Auto for every language, in every attempt, including retries.
| Agent | Agent ID | Speed | Strengths | Your Price |
|---|---|---|---|---|
| Qwen 3 235B AUTO (default) | qwen3-235b |
Fast | Excellent with Chinese, Japanese and Korean, and strong across every script we support. The everyday choice. | $3.00 / pair |
| DeepSeek V3 | deepseek-v3 |
Fast | Strong with European languages and dense technical content such as API references. | $3.00 / pair |
| Mistral Large | mistral-large |
Fast | Native-quality Romance languages: French, Spanish, Portuguese and Italian, idioms and formal register included. | $3.00 / pair |
| Claude Haiku 5.5 | claude-haiku-5.5 |
Fast | A versatile all-rounder with excellent instruction-following, well suited to high-volume jobs. | $3.00 / pair |
| Claude Sonnet 4.6 | claude-sonnet-4.6 |
Moderate | Deep reasoning and nuanced output. Handles complex scripts such as Arabic, Hindi and Japanese, and demanding technical documents. | $3.00 / pair |
| Claude Opus 4.8 | claude-opus-4.8 |
Slow | Maximum fidelity for critical documents, nuanced content, and languages with complex grammar. | From $3.00 / pair |
Integrations written against earlier agent IDs keep working: claude-haiku-3.5 and claude-opus-4 are still accepted and converted quietly to claude-haiku-5.5 and claude-opus-4.8.
All six agents share the same protection: sentinel preprocessing removes code blocks, brand terms, version numbers and technical identifiers before the agent sees your document, and the same validators check every section no matter which agent produced it. What changes between agents is the depth of linguistic reasoning and the speed, never the safety of your content.
AI translation that understands the difference between human language and machine language.
Your code blocks stay code. Your version numbers stay exact. Your brand names stay yours. Validated before delivery — not after you find the bug in production.
Want to see the quality? You're looking at it.
This website — cpmp-site.org — is published in 12 languages, and its technical documentation library was translated by the same engine we're offering you: 473 translated documents (43 documents in each of 11 languages) in production today.
Switch languages in the header. Read the Document Library in Japanese. Check the API Reference in German. Browse the Infrastructure Specification in Arabic, right to left. Code blocks intact. Version numbers exact. Brand names preserved.
This isn't a demo. This is production proof at scale.
The service translates HTML documents that you publish at a public URL. HTML is where technical documentation lives, and it is the format the engine understands most deeply: it knows a code block from a paragraph, a diagram from a table, and a translate="no" zone from prose.
| Content | How It Is Handled |
|---|---|
| Prose, headings, lists, table text | Translated |
| Code blocks and inline code | Protected — never shown to the agent, restored exactly |
| Mermaid diagrams | Protected structure; only human-readable labels are translated |
| URLs, email addresses, file paths, version numbers | Protected and verified after translation |
Anything inside translate="no" | Left exactly as written |
| Your brand terms (up to 150) | Never translated or transliterated (see 5.3) |
Working in Markdown, PDF or another format? Convert it to HTML first and the engine takes it from there.
You can translate into any of 127 languages: every language our agents on Amazon Bedrock can translate well, English included. Any of them can also be the source, and the engine translates directly from one to the other, so an Italian manual can go straight to English, or a Japanese guide straight to Spanish, with no pivot through a third language.
The list below is generated from the same catalog the service checks on every quote, and GET /translate/languages (3.11) returns it live. A language that is not on it is refused before anything is priced or charged, so you are never billed for a language we cannot translate.
dir="rtl" so it displays correctly.pt-PT), Mexican Spanish (es-MX), Canadian French (fr-CA), Traditional Chinese (zh-TW), Cantonese (yue) and the main Arabic dialects such as Egyptian (arz) each have their own code.nb, zh-Hant or en-US and we quietly use the matching code (no, zh-TW, en). Arabic with a country tag, such as ar-EG or ar-SA, means Modern Standard Arabic (ar), the formal Arabic of business and technical writing. For a dialect, use its own code.| Code | Language | Code | Language | Code | Language |
|---|---|---|---|---|---|
af | Afrikaans | sq | Albanian | am | Amharic |
ar | Arabic | arz | Arabic (Egyptian) | apc | Arabic (Levantine) |
acm | Arabic (Mesopotamian) | ary | Arabic (Moroccan) | ars | Arabic (Najdi) |
acq | Arabic (Ta'izzi-Adeni) | aeb | Arabic (Tunisian) | hy | Armenian |
as | Assamese | ast | Asturian | awa | Awadhi |
az | Azerbaijani | ban | Balinese | bjn | Banjar |
ba | Bashkir | eu | Basque | be | Belarusian |
bn | Bengali | bho | Bhojpuri | bs | Bosnian |
bg | Bulgarian | my | Burmese | yue | Cantonese |
ca | Catalan | ceb | Cebuano | hne | Chhattisgarhi |
zh | Chinese (Simplified) | zh-TW | Chinese (Traditional) | hr | Croatian |
cs | Czech | da | Danish | fa-AF | Dari |
nl | Dutch | en | English | et | Estonian |
fo | Faroese | fi | Finnish | fr | French |
fr-CA | French (Canada) | fur | Friulian | gl | Galician |
ka | Georgian | de | German | el | Greek |
gu | Gujarati | ht | Haitian Creole | ha | Hausa |
he | Hebrew | hi | Hindi | hu | Hungarian |
is | Icelandic | ilo | Ilocano (Iloko) | id | Indonesian |
ga | Irish | it | Italian | ja | Japanese |
jv | Javanese | kea | Kabuverdianu | kn | Kannada |
kk | Kazakh | km | Khmer | ko | Korean |
lo | Lao | lv | Latvian | lij | Ligurian |
li | Limburgish | lt | Lithuanian | lmo | Lombard |
lb | Luxembourgish | mk | Macedonian | mag | Magahi |
mai | Maithili | ms | Malay | ml | Malayalam |
mt | Maltese | mr | Marathi | min | Minangkabau |
mn | Mongolian | ne | Nepali | no | Norwegian (Bokmål) |
nn | Norwegian (Nynorsk) | oc | Occitan | or | Odia (Oriya) |
pag | Pangasinan | pap | Papiamento | ps | Pashto |
fa | Persian | pl | Polish | pt | Portuguese (Brazil) |
pt-PT | Portuguese (Portugal) | pa | Punjabi | ro | Romanian |
ru | Russian | sc | Sardinian | sr | Serbian |
scn | Sicilian | szl | Silesian | sd | Sindhi |
si | Sinhala | sk | Slovak | sl | Slovenian |
so | Somali | es | Spanish | es-MX | Spanish (Mexico) |
su | Sundanese | sw | Swahili | sv | Swedish |
tl | Tagalog (Filipino) | tg | Tajik | ta | Tamil |
tt | Tatar | te | Telugu | th | Thai |
tpi | Tok Pisin | tr | Turkish | uk | Ukrainian |
ur | Urdu | uz | Uzbek | vec | Venetian |
vi | Vietnamese | war | Waray | cy | Welsh |
yi | Yiddish (Eastern) |
One document translated into one language is one pair. Ten documents into eleven languages is 110 pairs, which is 330.00. Because the rate is flat, you can work out the price yourself before you ever ask for a quote, and that predictability is the point.
For full transparency, here is the underlying Amazon Bedrock cost behind each agent for a typical 50 KB document in one language. This is our cost, not your price, and the figures below load live from GET /translate/models. The difference between these figures and the flat $3.00 is what keeps the engine running, the validators thorough, and the mission funded.
| Agent | Underlying AI Cost (50 KB document, one language) |
|---|---|
| Qwen 3 235B | ~$0.02 |
| DeepSeek V3 | ~$0.04 |
| Mistral Large | ~$0.04 |
| Claude Haiku 5.5 | ~$0.01 |
| Claude Sonnet 4.6 | ~$0.34 |
| Claude Opus 4.8 | ~$0.57 |
Translating more than 1,000 pairs a month? Contact us and we will work out a rate with you.
A typical technical document translated into 11 languages with us is 11 pairs, which is 33.00, delivered in minutes.
| Option | What You Get |
|---|---|
| Trinity Beast Translation | Code, diagrams, versions and brand names protected and validated before delivery. A fixed price you see before you pay. Minutes, not weeks. |
| Generic machine translation (AWS Translate, Google Cloud Translation, DeepL) | Cheaper per word and fast, but built for prose. Code blocks, identifiers and version numbers get translated or dropped, and nothing tells you it happened. |
| Human translation | High-quality prose, but priced per word, measured in weeks, and still needs technical review to catch damaged code. |
The value proposition: you are not paying for translation alone. You are paying for translation that does not break your documentation, with a price you know in advance and validation you never have to do yourself.
Each limit exists to keep quality consistent and every job on time. Need more than one quote allows? Create another quote, there is no limit on how many you can make.
| Limit | Value | What It Means |
|---|---|---|
| Document size | 300 KB | Maximum HTML size per document (307,200 bytes). We check it when you ask for the quote and again when you accept, and a larger document is refused with tbts_doc_too_large, never cut short, so you are never charged for half a page. Split larger documents into smaller pages. |
| Languages per quote | 12 | Up to 12 target languages in one quote. The engine runs one container per language, so all of them translate in parallel. |
| Documents per quote | 42 | Up to 42 documents in one quote. |
| Quotes per batch | 9 | Accept up to 9 quotes together: one charge, one receipt, one completion email. |
| Quote validity | 72 hours | A quote can be accepted for 72 hours after it is created. After that, request a fresh one. If a document's content changed in the meantime, see 3.4. |
| Retry for failed pairs | 1 free retry | One retry per quote covers every retryable pair that didn't deliver, at no charge. A pair skipped because the document was already in that language is never retried; it is refunded instead (3.2). |
| Failed-pair refund window | 72 hours | A pair that didn't deliver is refunded automatically 72 hours after the job finishes, unless you retry it first. You can also ask for the refund straight away (3.9). |
| Price per pair | $3.00 | On five of the six agents, regardless of size. Claude Opus 4.8 is priced at its actual cost with a $3.00 floor, so a larger document can cost more per pair: on today's rates that starts at about 200 KB, and a 300 KB document comes to about $4.47 per pair. Your quote always shows the exact amount. |
| Delivery | Express | Every job runs in real time from the moment you accept. |
| Parallel workers | 12 | Every language in a job gets its own worker, up to 12 at once, so a 12-language quote never leaves a language waiting. Accepted jobs are never turned away: they wait in the queue and start as workers free up. |
| Daily spend cap | $600 | Engine-wide cost protection on AI spend. It is checked before your card is charged: if it, or the service's safety switch, is ever tripped, accepting answers 503 tbts_service_paused with nothing charged or queued, and you can try again later. |
| File availability | 93 days | Translated files are kept for 93 days after the job. Download links last 7 days, and you can request fresh ones any time in that window. |
The flow has three steps and you are in control at every one: quote (see the exact price), accept (pay and start), download (collect your files). You can do all of it from the Account Dashboard without writing any code, or automate it with the endpoints below.
Every request uses your translation API key in the X-API-Key header:
curl -H "X-API-Key: your-api-key-here" \
https://api.cpmp-site.org/translate/quotes
Your key lives in your Trinity Beast Account Dashboard. Log in at cpmp-site.org/dashboard with the email you used to sign up. Translation-only customers are first-class citizens: no Price API subscription is required, and if your email also has other products you will see everything in one view.
Every response, success or error, uses the Unified Messaging Envelope. All 12 fields are always present.
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/translate/quote] [200]",
"status_code": 200,
"endpoint": "/translate/quote",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ak_demo123",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-10-09T18:30:00Z",
"data": { ... },
"error": ""
}
The language field reflects your api_lang preference, and error messages arrive in that language. Envelope metadata stays English always, and every response tells you which container handled it, in which region, and exactly when.
POST /translate/quote
Send the documents and the languages you want. The engine fetches each document, analyzes it, works out what language it is written in, and returns a fixed price. Nothing is charged at this step.
{
"docs": ["https://example.com/docs/guide.html", "https://example.com/docs/faq.html"],
"langs": ["en", "es", "zh-TW"],
"source_lang": "it",
"model": "qwen3-235b"
}
| Field | Type | Required | Description |
|---|---|---|---|
docs | array | Yes | Public URLs of the HTML documents to translate (up to 42, each up to 300 KB). A single doc_url string is also accepted. Each document's name is the last part of its URL path, and two documents in one quote can't share a name. |
langs | array | Yes | Target language codes from 1.5, up to 12. English is a target like any other. Duplicates and alternate codes are tidied up for you. |
source_lang | string | No | The language your documents are written in, if you know it. Applies to every document without its own entry in source_langs. Use "auto", or leave it out, to let us work it out. |
source_langs | object | No | Per-document source language: {"<doc url>": "uk"}. Beats source_lang for that document. |
model | string | No | Agent ID from 1.2. Leave it out and the engine picks per language (1.2). |
supersedes_quote_id | string | No | The ID of one of your pending quotes that this new quote replaces, for example after you add a source language. The old quote is marked superseded and the response's superseded_quote_id names it. Any other ID is ignored and the new quote still stands. |
Two things depend on knowing what language a document is written in: the agent is told to translate from that language, and we can stop you paying to translate a document into the language it is already in. So the engine takes the source from the most reliable place available:
source_lang or source_langs) always comes first.<html lang="..."> comes next. If what we can tell for certain from the text (step 3) contradicts it, for example a Japanese page that declares English, the document is marked auto rather than trusted.An unsure source never blocks your order. The quote still succeeds, marks that document source_lang_confirm_needed: true, and includes a hint in your language. Add source_langs and request the quote again if you know the answer; otherwise the agent translates from the document's original language as written. In the Account Dashboard you're simply asked "What language is this document written in?", with an "I'm not sure" option that always lets you continue.
A document can't be translated into its own language: an Italian page with target it is refused with a clear message, before anything is charged. Related variants are real conversions and always allowed, such as Spanish to Mexican Spanish, Simplified to Traditional Chinese, or Modern Standard Arabic to Egyptian.
There is one more safety net behind the quote. If we couldn't tell a document's language (auto) and the worker later recognizes for certain that it is already in one of your targets, it does not translate that pair. The pair is refunded automatically at its own price, so you never pay to receive your own page back.
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"docs": [
{
"doc_url": "https://example.com/docs/guide.html",
"doc_name": "guide.html",
"analysis": { "size_bytes": 63488, "estimated_chunks": 4, "difficulty": "moderate", "code_blocks": 12, "diagrams": 2 },
"total_price": 9.00
}
],
"sources": [
{ "doc_url": "https://example.com/docs/guide.html", "doc_name": "guide.html", "source_lang": "it", "source_provenance": "stated", "source_lang_confirm_needed": false, "source_hint": "" }
],
"doc_count": 1,
"languages": ["en", "es", "zh-TW"],
"lang_count": 3,
"total_pairs": 3,
"model": "qwen3-235b",
"auto_routed": false,
"model_by_lang": {},
"delivery": { "mode": "express", "label": "Express — real-time translation", "estimated_time": "~6 minutes", "qualified_reason": "3 translated documents (1 docs × 3 languages)" },
"pricing": { "price_per_pair": 3.00, "total_pairs": 3, "total_price": 9.00, "currency": "USD" },
"expires_at": "2026-10-13T18:30:00Z",
"accept_url": "/translate/accept/qt-a1b2c3d4-e5f",
"superseded_quote_id": ""
}
The analysis block shows what the engine found in each document: its size, how many sections it will translate, how many code blocks and diagrams it will protect. sources shows the source language for every document and where it came from (stated, declared, detected or auto). You see exactly what you are buying before you buy it.
GET /translate/quote/{quote_id} returns one quote: its state (pending, accepted, expired, superseded when a newer quote replaced it, refunded, or partially_refunded when part of the amount came back to you after a cancel or a failed pair), every document, the pair count, each document's source language, the price, the expiry, and the job_id once it has been accepted.
GET /translate/quotes returns your 30 most recent quotes, newest first, each with quote_id, doc_name, lang_count, total_price, state, expires_at, job_id and model.
POST /translate/accept/{quote_id}
Accepting charges the card on file for the quoted total and starts translation immediately. No request body is needed.
"data": {
"status": "accepted",
"quote_id": "qt-a1b2c3d4-e5f",
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"model": "qwen3-235b",
"payment_id": "pi_3abc123def456",
"amount_usd": 9.00,
"status_url": "/translate/status/019e4b8a37bc-1f2e3d4c5b6a7980",
"notice": "",
"notice_key": "",
"updated_docs": [],
"key": "",
"message": "",
"refunded": false,
"refund_amount": 0
}
Keep the job_id: status_url follows it live (3.6), and it is what you use to download the result. Every field is always present, so your code can read the same shape whatever happens.
A charge and a start are two separate events, and the response tells you honestly which ones happened:
status: "accepted": you were charged and the job started.status: "pending" (key: "tbts_translation_start_pending"): you were charged and we are still confirming the job started. If it didn't, the full amount is refunded automatically. status_url uses the quote ID (/translate/status/qt-a1b2c3d4-e5f) so you can follow it either way.status: "refunded" (tbts_translation_start_refunded): the payment went through but the job couldn't start, so the full amount has already been refunded. refunded is true and refund_amount shows how much.status: "refund_delayed" (tbts_status_refund_delayed): the job couldn't start and the refund is on its way but taking longer than usual. It is retried automatically and our team is alerted.A quote can be accepted for up to 72 hours, and a web page can change in that time. So before your card is touched, the engine fetches every document again and compares its readable content with what was priced. Invisible markup such as scripts, styles, tracking tags and timestamps is ignored, so a page that reloads slightly differently every time doesn't count as changed.
notice tells you which documents were updated.tbts_quote_requoted with a fresh quote attached (new_quote_id, new_total_price, changed_docs, accept_url and the full quote), ready to accept. The old quote is marked superseded.tbts_doc_too_large) or now declares the same language as one of your targets (400 tbts_target_is_source): nothing is charged and no new quote is made, because no price would be honest. The old quote is marked superseded; fix the document or the languages and ask for a new quote.Two accepts of the same quote at the same moment, from a double-click or a client retry, can never charge you twice. The first accept claims the quote, and the second answers at once with 409 tbts_quote_accept_in_progress ("Quote [quote_id] is already being accepted. Please wait a minute, then check it before trying again."), with nothing staged or charged. If a paid job ever fails to start, you're refunded in full automatically, and GET /translate/status works with the quote ID so you can follow it.
POST /translate/accept-batch with {"quote_ids": [ ... ]} accepts up to 9 quotes in one step: one charge, one receipt, and one completion email when the whole batch is done. Every quote is checked, and every document re-fetched, before your card is touched (see 5.5). If any quote needs a fresh price, the whole batch is held with nothing charged, and the response lists each replacement quote so you can resubmit in one go. Each quote becomes its own job inside one Task (see 5.6). The response's task_status_url (/translate/task/{task_id}) follows the whole batch, and each job carries its own status_url (3.6).
Follow a job from the Account Dashboard, where every document-language pair has its own chip that moves from queued to translating to delivered, or from your own code:
GET /translate/status/{job_id} (your translation key; a quote ID also works)
"data": {
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "quote_id": "qt-a1b2c3d4-e5f", "parent_job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"state": "partial", "job_state": "partial",
"model": "qwen3-235b", "model_by_lang": {}, "model_display": "Qwen 3 235B",
"submitted_at": "2026-10-10T18:31:02Z", "started_at": "2026-10-10T18:31:09Z", "completed_at": "2026-10-10T18:40:00Z",
"docs": ["guide.html"], "langs": ["en", "zh-TW"], "total_pairs": 2,
"counts": { "queued": 0, "running": 0, "succeeded": 1, "failed": 1, "cancelled": 0 },
"pairs": [
{ "doc": "guide.html", "lang": "en", "state": "succeeded", "job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "attempts": 1 },
{ "doc": "guide.html", "lang": "zh-TW", "state": "failed", "job_id": "019e4b8a37bc-1f2e3d4c5b6a7980", "attempts": 1 }
],
"eta": null,
"retry": { "allowed": true, "state": "open", "retry_job_id": "", "window_ends_at": "2026-10-13T18:40:00Z" },
"cancel_refund": { "status": "", "amount_due": 0, "amount_refunded": 0 },
"failed_pair_refund": { "status": "window_open", "amount_due": 3.00, "amount_refunded": 0, "pairs": [{ "doc": "guide.html", "lang": "zh-TW" }], "window_ends_at": "2026-10-13T18:40:00Z", "refunded_at": "" },
"refund_status": "window_open", "refund_due": 3.00, "total_refunded": 0,
"downloads_ready": true, "download_url": "/translate/download/019e4b8a37bc-1f2e3d4c5b6a7980",
"message_key": "tbts_status_partial_retry_open",
"message": "1 of 2 translations couldn't be completed. You can retry them at no charge until 2026-10-13 18:40 UTC. If you don't, we'll refund 3.00 automatically."
}
Every field is always present. state is one of queued, running, succeeded, partial, failed or cancelled; eta is filled while the job runs; message explains where things stand in your api_lang. You only ever see your own jobs, from any of your keys, and anything else answers 404.
GET /translate/task/{task_id} does the same for a whole batch: overall state, totals across every job, and each job's full status.
When a job finishes you also receive one email with your download links (see 5.2).
GET /translate/download/{job_id}
Once the job has finished, this returns a download link for every translated file:
"data": {
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"quote_id": "qt-a1b2c3d4-e5f",
"state": "succeeded",
"doc_name": "api-reference.html",
"doc_names": ["api-reference.html", "faq.html"],
"doc_count": 2,
"expires_in": "7 days",
"files_available": "93 days from job completion — request a fresh link anytime within that window",
"downloads": [
{ "doc": "api-reference.html", "language": "es", "url": "https://trinity-beast-translations.s3...", "key": "jobs/qt-a1b2c3d4-e5f/output/es/api-reference.html", "filename": "es-api-reference.html" },
{ "doc": "api-reference.html", "language": "ja", "url": "https://trinity-beast-translations.s3...", "key": "jobs/qt-a1b2c3d4-e5f/output/ja/api-reference.html", "filename": "ja-api-reference.html" },
{ "doc": "faq.html", "language": "es", "url": "https://trinity-beast-translations.s3...", "key": "jobs/qt-a1b2c3d4-e5f/output/es/faq.html", "filename": "es-faq.html" },
{ "doc": "faq.html", "language": "ja", "url": "https://trinity-beast-translations.s3...", "key": "jobs/qt-a1b2c3d4-e5f/output/ja/faq.html", "filename": "ja-faq.html" }
]
}
Every document in the quote gets its links, and so does every language that delivered, even when another failed or the job was cancelled partway. Each file saves as {lang}-{doc}, such as es-faq.html, so a folder of downloads sorts itself by language. Each link is private to you and lasts 7 days. Your files are kept for 93 days, so you can ask for fresh links whenever you need them. A job that is still running returns 409 with its current state, and the same links are always one click away in the dashboard.
POST /translate/cancel/{quote_id}
You stay in control after you pay. Cancel any time before the job finishes, from the API or with the Cancel Translation button in your Account Dashboard, and you pay only for what you received. Every document-language pair already delivered is yours to keep. Every pair that was not delivered is refunded at its full per-pair price.
curl -X POST -H "X-API-Key: your-api-key-here" \
https://api.cpmp-site.org/translate/cancel/qt-a1b2c3d4-e5f
"data": {
"quote_id": "qt-a1b2c3d4-e5f",
"job_id": "019e4b8a37bc-1f2e3d4c5b6a7980",
"state": "cancelled",
"refund_status": "calculating",
"message": "Translation cancelled. Pages already delivered are yours to keep. The refund for the rest is being calculated."
}
The engine never cuts a language off mid-sentence. Each language finishes the section it is working on and stops at that boundary, so every page you receive is complete and every page you do not receive is refunded. You will never be charged for, or handed, half a translation. On a large document one section can take up to about three minutes, and the dashboard tells you it is finishing rather than leaving you to wonder.
When the last language stops, the refund is counted from what was actually delivered and moves through calculating → pending_approval → approved. A person reviews every refund before it is issued, and you get one email, in your language, showing what was delivered, what was stopped, and the amount coming back to you. If every pair had already been delivered, the state is none: there is nothing to refund because all of it is yours.
If your free retry (3.9) is the job that is running, cancelling stops only the retry. You get 200 with tbts_cancel_retry_accepted ("The retry was cancelled. Anything it didn't finish will be refunded automatically."), refund_status stays empty, and there is no approval step: the retry was free, so whatever neither the job nor the retry delivered is simply refunded automatically, the same way as in 3.9.
Fair in both directions: refund = total paid − (delivered pairs × per-pair price), counted in whole cents with any rounding going to you. If a language was partway through a section when you cancelled, we absorb the AI cost of that partial work. A cancelled job stays in your Translation History with its refunded amount, so your records always tell the whole story.
Returns 409 Conflict if the translation has already finished or was already cancelled, and 404 if the quote is not on your account. The full contract, including every error, is in the API Reference §15.7.
Every section is checked before it ships, and a pair that can't pass is reported as failed instead of being delivered damaged. When that happens you never pay for it:
POST /translate/retry/{quote_id}, or the Retry button in the dashboard, runs just the failed pairs again, on the agent you chose, at no charge. One retry covers every retryable failed pair in the quote. A pair skipped because the document was already in that language (3.2) is never retried, only refunded.POST /translate/refund-failed/{quote_id}, or "Refund instead" in the dashboard, refunds the failed pairs straight away. Until automatic refunds for incomplete translations are switched on, this one endpoint answers 404 tbts_refund_failed_unavailable; the free retry is always available.Each failed pair is refunded at exactly the price you paid for it, counted in whole cents with any rounding going to you. A pair the retry delivers is yours and isn't refunded, and once a pair is refunded its retry is closed, so nothing is ever both delivered and refunded, or refunded twice. You get one email in your language when the refund is issued, and failed_pair_refund in the status (3.6) shows each step. Unlike a cancel (3.8), these refunds need no approval: the pair didn't deliver, so the money comes back.
GET /translate/models (public, no key needed) lists the six agents with their IDs, names, speed and the underlying AI cost from 2.2. Use an id as the model in your quote; the response's default field names the agent used when you leave it out.
"data": {
"models": [
{
"id": "qwen3-235b",
"name": "Qwen 3 235B",
"tier": "Fast & Capable",
"input_rate_per_1m": 0.22,
"output_rate_per_1m": 0.88,
"estimated_cost_per_pair": 0.019294,
"speed": "fast",
"quality": "good",
"description": "Fast and capable. Excellent with Chinese, Japanese, and Korean. The default choice."
}
],
"default": "qwen3-235b",
"pricing_note": "Rates are per 1M tokens. estimated_cost_per_pair is for a typical 50KB document (one language). Actual cost depends on document size and content density."
}
The rates and estimated_cost_per_pair are our AI cost, shown for transparency. Your price is the flat $3.00 per pair in 2.1.
GET /translate/languages (public, no key needed) returns the live catalog behind 1.5: every language with its code, English and native name, script, and whether it is right to left, plus the current limits and the alternate codes we accept. count is the number of languages you can order today, so your integration never has to hard-code the list.
"data": {
"count": 127,
"languages": [
{ "code": "af", "english_name": "Afrikaans", "native_name": "Afrikaans", "script": "Latn", "rtl": false, "word_spaced": true, "base_lang": "", "status": "supported" }
],
"limits": { "max_langs_per_quote": 12, "max_docs_per_quote": 42, "max_doc_kb": 300, "max_quotes_per_batch": 9 },
"aliases": { "nb": "no", "zh-Hant": "zh-TW", "ar-EG": "ar" },
"source": "aurora"
}
The service runs on the same engine that translates our own documentation library every day, so the path your document takes has been exercised hundreds of times before it reaches you. The journey has two halves, and each has its own diagram. Diagram 4.1 is everything that happens before and at the moment you pay: the quote, the source language, and the second look at every document before your card is touched. Diagram 4.2 is everything after: the translation itself, live status, the free retry and the automatic refunds that make sure you only pay for what you receive.
sequenceDiagram
participant C as Customer
participant L as LPO Server (Go)
participant S3 as S3 (Your Files)
participant St as Stripe
participant Q as SQS Queue
rect rgba(96, 165, 250, 0.16)
C->>L: POST /translate/quote (API key)
L->>L: Check limits and the 127-language catalog
L->>L: Fetch each document (up to 300 KB) and fingerprint it
L->>L: Source language: yours, the page lang, a sure detection, or auto
L-->>C: Quote (fixed price, valid 72h) with every source language
end
rect rgba(251, 191, 36, 0.14)
C->>L: POST /translate/accept/{quote_id}
L->>L: Claim the quote (a second accept gets 409)
L->>L: Re-check limits and service guards (503, no charge)
L->>L: Fetch every document again and compare
end
alt Price or source result changed
L-->>C: 409 with a new quote attached, nothing charged
else Page now breaks a rule (over 300 KB, or target is source)
L-->>C: 400, no new quote, nothing charged
else Same content, or same price and rules
rect rgba(52, 211, 153, 0.14)
L->>S3: Store the checked source documents
L->>St: Charge the card on file
St-->>L: Payment succeeded
L->>Q: One message per language
end
alt Job started
L-->>C: 200 with status_url (and a notice if content changed)
else Not confirmed yet
L-->>C: 202 pending, full refund if it never starts
else Job could not start
L->>St: Full refund, automatic
L-->>C: 502 refunded
end
end
sequenceDiagram
participant C as Customer
participant L as LPO Server (Go)
participant Q as SQS Queue
participant W as BeastTranslate Worker
participant S3 as S3 (Your Files)
participant St as Stripe
rect rgba(167, 139, 250, 0.16)
Q->>W: Each worker takes one language
W->>W: Protect, translate, validate (every language in parallel)
W->>S3: Write translated files
W->>L: Job complete
L-->>C: Completion email with download links
end
rect rgba(96, 165, 250, 0.16)
loop While you want to know
C->>L: GET /translate/status/{job_id} or /translate/task/{task_id}
L-->>C: Every pair, the ETA and any refund
end
end
opt Some pairs failed
rect rgba(251, 191, 36, 0.14)
C->>L: POST /translate/retry/{quote_id} (one free retry)
L->>Q: Only the retryable failed pairs, same agent
end
rect rgba(52, 211, 153, 0.14)
Note over L,St: Refund sweeper, every 15 minutes
L->>St: Refund what is still undelivered (after the retry, Refund instead, or 72h)
St-->>L: Refund issued once (idempotency key txfr-quote_id)
L-->>C: Refund email
end
end
Note over L,St: The same sweeper refunds in full a paid quote that never started (202)
C->>L: GET /translate/download/{job_id}
L-->>C: Private download links (7 days)
Key components:
txfr-{quote_id}), so it can never be sent twicelang, then a definitive detection) and detects only when the quote said auto. It refuses to translate a pair already in its target language, lifts out protected content, splits the document at natural section boundaries, translates with your chosen agent, validates every section, and restores everything it protectedEvery translated section passes these checks before it is accepted:
| Validator | What It Checks |
|---|---|
| Protected Terms | Brand and product names survive unchanged |
| Term Spacing | A protected term has not been glued onto the next word (skipped for the 8 languages that do not separate words with spaces: Japanese, Chinese, Traditional Chinese, Cantonese, Thai, Lao, Khmer and Burmese) |
| Version Numbers | 17.7, 4.6, v2.0 come through exactly |
| Preserved Values | URLs, IP addresses, ARNs, email addresses and other technical identifiers are unchanged |
| Tag Counts | The HTML structure matches the source, so nothing is dropped or doubled |
| Protected Zones | Content inside translate="no" is byte-for-byte unchanged |
| Degeneration | The output has not collapsed into repetition or truncation |
A section that fails is retried, with a directed correction when the engine knows exactly which rule was broken. A pair that still cannot pass is reported as failed rather than delivered damaged. You can retry just the failed pairs at no extra charge, and whatever still can't be delivered is refunded automatically (see 3.9).
The key insight: AI models make the right kind of mistakes — visible and recoverable. The validators catch them before anything ships. Generic translation services fail silently and structurally, and you only find out by reading every page yourself.
No pattern-matching guesswork: the structural validators walk the text character by character with exact boundary matching. We control these tags and markers, so the checks know precisely where each one starts and ends. No partial matches, and no false alarms from text that merely looks similar.
Each accepted quote gets its own private folder:
s3://trinity-beast-translations/
jobs/{quote_id}/
source/api-reference.html (your original, as checked at accept)
source/faq.html
output/es/api-reference.html (Spanish)
output/es/faq.html
output/ja/api-reference.html (Japanese)
output/ja/faq.html
Your source documents are written here when you accept, after the second check and before your card is charged, so the job translates exactly the content whose price was verified, and nothing else. A free retry writes into the same folder, so a pair it delivers sits right beside the ones that delivered the first time. When you download, each file is named {lang}-{doc}, such as ja-faq.html.
Files are kept for 93 days and removed automatically after that. Download links are signed, private to you, and valid for 7 days, the longest a signed link can last, and you can request fresh ones any time within the 93 days.
Everything that could make a job fail is checked when you ask for the quote, and checked again when you accept, so you never pay for work that cannot run. The quote and the job read the same limits from one place in the code, so a quote can never promise something the job would refuse.
tbts_doc_too_large, never cut short.The quote also carries the engine's analysis of each document: its size, the number of sections it will be translated in, its difficulty, and how many code blocks and diagrams it will protect. Splitting happens at natural boundaries such as headings, tables and code blocks, never in the middle of a paragraph, which gives the agent whole thoughts to work with and you better translations.
We built this to be watched and steered, not to disappear for ten minutes behind a job ID.
POST /translate/cancel/{quote_id}, or one click in the dashboard, stops the job cleanly. Delivered pages are yours and undelivered pairs are refunded in full (see 3.8).GET /translate/status/{job_id} and GET /translate/task/{task_id} report every pair, the ETA and any refund (3.6).The difference: generic batch services give you a job ID and say "check back later." We give you live progress, mid-job cancellation with a fair refund, free retry and automatic refunds for anything that couldn't be delivered, and a complete history. Observable. Controllable. Resilient.
We maintain a technical library of more than 40 documents translated into 11 languages, and those documents change as we ship. Rather than re-translating a whole document on every update, our internal pipeline identifies exactly which sections changed and processes only those.
This is an internal optimization, not a customer feature: you always submit a document and receive complete translations. It matters to you because it keeps our own costs low, which keeps your price flat, and because the engine translating your document is exercised every week on ours.
The Trinity Beast Account Dashboard is where you manage everything. Translation-only customers are first-class citizens: you do not need a Price API subscription to use translation.
Dashboard URL: cpmp-site.org/dashboard
When you log in you see everything associated with that email: REST API subscriptions, translation keys, webhook price feeds and recurring giving, in one view. To separate services by department or cost center, use a different email for each; every email is its own account with its own billing portal, invoices and payment method.
When a job finishes you receive one email, in your communication language, with everything you need: the result for every document and language, the agent that translated it, how long it took, what you were charged, and a download link for every file. If any pair failed, the email shows which ones, and you can retry just those at no extra charge. When a failed pair is refunded, a second email in your language confirms which pairs were refunded and the amount coming back to you.
A batch is one email too. When you accept several quotes together, you hear from us once, when the last job in the batch is done, rather than once per document (see 5.6).
Every account can protect up to 150 brand terms. They are never translated, transliterated or changed in any way; they appear exactly as you wrote them in every language.
Generic translation services do not know your brand names. "Acme Corp" becomes "アクメ株式会社" in Japanese and "DataForge" becomes "DatenSchmiede" in German. Product names, people's names and technical identifiers get localized when they should not.
| Constraint | Value |
|---|---|
| Maximum terms per account | 150 |
| Maximum characters per term | 100 |
| Duplicates | Removed automatically |
| Scope | Your whole account — every job |
<pre> and <code> elements)translate="no"Brand terms add protection where the engine cannot know on its own: names that appear inside ordinary sentences.
Example: with "CloudSync API" and "DataForge" as brand terms, "DataForge uses CloudSync API for real-time synchronization" becomes "DataForge は CloudSync API を使用してリアルタイム同期を行います" in Japanese — both names untouched, the sentence around them fully translated.
GET /dashboard/api/protected-terms → your current list
PUT /dashboard/api/protected-terms → replace the list
Body: {"terms": ["Acme Corp", "DataForge", "CloudSync API"]}
These use your dashboard session rather than your API key. Details are in the API Reference §15.14.
Every API response speaks your language. When you set your api_lang preference in the Account Dashboard, all human-readable messages — errors, confirmations, validation feedback — are returned in that language. Data elements stay English always.
Data stays English. Messages get localized.
Field names, IDs, status values, timestamps, amounts, quote IDs, job IDs — always English regardless of your language preference. Only the human-readable text (error descriptions, validation messages, informational labels) renders in your api_lang. This means your integration code never changes — parse the same JSON structure regardless of language.
| Category | Localized? | Example |
|---|---|---|
| Error messages | ✅ Yes | "La cotización ha expirado" (Spanish) |
| Validation feedback | ✅ Yes | "見積もりが見つかりません" (Japanese) |
| Email subjects & body | ✅ Yes | "Ihre Übersetzung ist fertig" (German) |
| Field names | ❌ No | quote_id, job_status, total_price |
| Status values | ❌ No | "accepted", "pending", "expired" |
| UME envelope brackets | ❌ No | ✅ [LPO] [us-east-2] [BeastMain] |
| Timestamps & amounts | ❌ No | "2026-07-08T14:00:00Z", "$3.96" |
Two language settings control your experience:
api_lang — Controls API response messages. Set in Account Dashboard → Settings → API Language. Default: en.preferred_lang — Controls emails (receipts, completion notifications, newsletters). Set in Account Dashboard → Settings → Communication Language. Default: en.These are the languages our messages speak, not the languages your documents can go into: your documents can be translated into any of the 127 languages in 1.5. API messages and emails are available in all 12 core languages:
en · es · pt · fr · de · ru · hi · ur · it · ar · ja · zh
If your api_lang is set to a language not in this list, messages fall back to English.
A customer with api_lang: "ja" sees validation errors in Japanese:
{
"status": "🛑 [LPO] [us-east-2] [BeastMain] [/translate/accept-batch] [409]",
"status_code": 409,
"endpoint": "/translate/accept-batch",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "ja",
"api_key_id": "ed5ff412-4de1-4fb4-a96d-1f7053834f61",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-07-08T14:00:00Z",
"data": {
"status": "validation_failed",
"message": "バッチ内の一部の見積もりが処理できませんでした。エラーを確認し、有効な見積もりのみで再送信してください。",
"errors": [
{
"quote_id": "qt-7c1e2a90-4b3",
"error": "見積もり qt-7c1e2a90-4b3 は有効期限が切れています。新しい見積もりをリクエストしてください。",
"key": "tbts_quote_expired",
"new_quote_id": "",
"new_total_price": 0
}
],
"valid": 2,
"invalid": 1
},
"error": ""
}
The same request from a customer with api_lang: "es":
{
"status": "🛑 [LPO] [us-east-2] [BeastMain] [/translate/accept-batch] [409]",
"status_code": 409,
"endpoint": "/translate/accept-batch",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "es",
"api_key_id": "ed5ff412-4de1-4fb4-a96d-1f7053834f61",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-07-08T14:00:00Z",
"data": {
"status": "validation_failed",
"message": "Algunas cotizaciones no pudieron procesarse. Revise los errores y reenvíe solo las cotizaciones válidas.",
"errors": [
{
"quote_id": "qt-7c1e2a90-4b3",
"error": "La cotización qt-7c1e2a90-4b3 ha expirado. Solicite una nueva.",
"key": "tbts_quote_expired",
"new_quote_id": "",
"new_total_price": 0
}
],
"valid": 2,
"invalid": 1
},
"error": ""
}
The integration contract stays identical. Parse data.errors[].quote_id and branch on data.errors[].key, then show data.errors[].error to your user — same structure, same field names, same logic. The only difference is the human-readable string values. Your code doesn't need language-specific branches.
The batch acceptance endpoint validates every quote BEFORE charging your payment method. If any quote in your batch is invalid, the entire batch is rejected with structured errors — no partial charges, no guessing which ones failed.
Validate everything. Charge nothing. Return all errors in one round-trip.
When you submit a batch of 9 quotes, all 9 are validated against Aurora before Stripe is touched. If quotes #3 and #7 have problems, you get both errors back immediately — fix them and resubmit. Your card is never charged for a batch that can't fully execute.
Every entry in errors has the same five fields, always present: {quote_id, error, key, new_quote_id, new_total_price}. key tells your code exactly what happened, error says it in your api_lang, and the last two are filled only when we prepared a replacement quote for you. The pre-validation gate catches these problems:
| Key | Meaning | Resolution |
|---|---|---|
tbts_quote_not_found | The quote ID does not exist or belongs to a different account | Verify the quote ID — check GET /translate/quotes for your active quotes |
tbts_quote_already_processed | This quote was already accepted and paid for (includes the date it was processed) | Remove it from your batch — the job is already running or complete |
tbts_quote_expired | The quote's 72-hour validity window has passed | Request a new quote — pricing may have changed |
tbts_quote_accept_in_progress | Another request is accepting this quote right now | Wait a minute, check the quote, then resubmit without it if it was accepted |
tbts_quote_needs_requote | The quote no longer passes today's checks, or a document couldn't be fetched again | Request a new quote for those documents |
tbts_quote_requoted | A document changed and the price or source result changed with it. new_quote_id and new_total_price name the replacement we prepared | Resubmit the batch with new_quote_id in place of the old ID |
A rule's own key, such as tbts_doc_too_large or tbts_target_is_source | A document now breaks a rule, for example it grew past 300 KB. No replacement quote is made | Fix the document or the languages, request a new quote, and resubmit |
The batch-level status: "validation_failed" and its message summarize the whole response, and valid / invalid count the quotes on each side.
When pre-validation fails, the response uses HTTP 409 with structured error detail:
{
"status": "🛑 [LPO] [us-east-2] [BeastMirror] [/translate/accept-batch] [409]",
"status_code": 409,
"endpoint": "/translate/accept-batch",
"cluster_node": "BeastMirror",
"region": "us-east-2",
"language": "en",
"api_key_id": "ed5ff412-4de1-4fb4-a96d-1f7053834f61",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-07-08T14:30:00Z",
"data": {
"status": "validation_failed",
"message": "Some quotes in your batch could not be processed. Please review the errors below and resubmit with valid quotes only.",
"errors": [
{
"quote_id": "qt-7c1e2a90-4b3",
"error": "Quote qt-7c1e2a90-4b3 has expired. Please request a new quote.",
"key": "tbts_quote_expired",
"new_quote_id": "",
"new_total_price": 0
},
{
"quote_id": "qt-91d0f3b2-6ae",
"error": "user-guide.html changed since you were quoted, so we've prepared a new quote: $36.00. You have not been charged.",
"key": "tbts_quote_requoted",
"new_quote_id": "qt-e5b27c08-9f1",
"new_total_price": 36.00
}
],
"valid": 7,
"invalid": 2
},
"error": ""
}
// Pseudocode — handling the 409 pre-validation response
response = POST("/translate/accept-batch", { quote_ids: [...] })
if response.status_code == 409:
// No charge was made. Fix the issues and retry.
for error in response.data.errors:
log(f"Quote {error.quote_id} failed: {error.key} {error.error}")
if error.key == "tbts_quote_requoted":
replace(quote_ids, error.quote_id, error.new_quote_id) // the fresh quote we prepared
else:
remove(quote_ids, error.quote_id)
retry_response = POST("/translate/accept-batch", { quote_ids: quote_ids })
elif response.status_code == 200:
// All quotes valid, payment processed, jobs submitted.
task_id = response.data.task_id
// Each job appears in your dashboard; one email arrives when the batch is done.
Idempotent by design: If a quote has already been processed, the error includes when it was processed and tells you to remove it. This prevents double-charging — even if your retry logic accidentally re-submits a completed quote, it will never be charged twice.
When all quotes pass validation and payment succeeds:
{
"status": "✅ [LPO] [us-east-2] [BeastMain] [/translate/accept-batch] [200]",
"status_code": 200,
"endpoint": "/translate/accept-batch",
"cluster_node": "BeastMain",
"region": "us-east-2",
"language": "en",
"api_key_id": "ed5ff412-4de1-4fb4-a96d-1f7053834f61",
"ip_address": "203.0.113.42",
"agent_profile_arn": "arn:tbi:us-east-2:211998422884:agent-profile/tbts/v1",
"timestamp": "2026-07-08T14:30:00Z",
"data": {
"status": "accepted",
"batch_size": 3,
"quote_ids": ["qt-7c1e2a90-4b3", "qt-91d0f3b2-6ae", "qt-c48a5e17-0d2"],
"task_id": "019f3b4d8e9f-0a1b2c3d4e5f6a7b",
"task_status_url": "/translate/task/019f3b4d8e9f-0a1b2c3d4e5f6a7b",
"jobs": [
{"quote_id": "qt-7c1e2a90-4b3", "doc_name": "api-reference.html", "job_id": "019f3c5a0b1c-2d3e4f5a6b7c8d9e", "error": "", "key": "", "status_url": "/translate/status/019f3c5a0b1c-2d3e4f5a6b7c8d9e", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": []},
{"quote_id": "qt-91d0f3b2-6ae", "doc_name": "user-guide.html", "job_id": "019f3c6b1c2d-3e4f5a6b7c8d9e0f", "error": "", "key": "", "status_url": "/translate/status/019f3c6b1c2d-3e4f5a6b7c8d9e0f", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": []},
{"quote_id": "qt-c48a5e17-0d2", "doc_name": "quick-start.html", "job_id": "019f3c7c2d3e-4f5a6b7c8d9e0f1a", "error": "", "key": "", "status_url": "/translate/status/019f3c7c2d3e-4f5a6b7c8d9e0f1a", "refunded": false, "refund_amount": 0, "notice": "", "notice_key": "", "updated_docs": []}
],
"payment_id": "pi_3TgvABC123...",
"amount_usd": 99.00,
"eta": {
"estimated_seconds": 540,
"estimated_low_seconds": 420,
"estimated_high_seconds": 660,
"estimated_formatted": "~9 minutes",
"estimated_range": "7-11 minutes",
"bottleneck_lang": "ar",
"elapsed_seconds": 0,
"remaining_seconds": 540,
"remaining_formatted": "~9 minutes",
"percent_complete": 0
},
"key": "",
"message": ""
},
"error": ""
}
In this example three quotes of 11 pairs each come to one charge of 99.00. The task_id identifies the batch as a unit, task_status_url follows all of it at once, and each job inside it appears in your dashboard with its own progress and downloads. The eta is the slowest job in the batch, so it tells you when everything will be done.
Each entry in jobs reports that quote's own start, so one quote that can't start never holds the others back. A quote that couldn't start is refunded in full on its own: its job_id is empty, key is tbts_translation_start_refunded (or tbts_translation_start_pending while we confirm), refunded and refund_amount show what came back, and its status_url uses the quote ID. If no quote in the batch could start, the response is 502 with status: "start_failed" and the same jobs list.
When you accept several quotes in one batch, the engine creates a parent Task that tracks all of the resulting jobs as a unit. One acceptance, one Task, one notification when everything is finished.
Without it, accepting 9 quotes would mean 9 separate completion emails arriving one by one. That is noise, not signal. The Task Organizer waits until every job in the batch has finished and then tells you once.
The task_id in the accept-batch response identifies the batch, and each job inside it appears in your dashboard with its own live progress and downloads.
The batch email arrives in your communication language and contains:
GET /translate/download/{job_id} returns the links for any jobEvery quote in a batch is its own job, so you cancel with the same precision you accepted with: send POST /translate/cancel/{quote_id} for each quote you want to stop. The jobs you leave alone carry on untouched, finished jobs stay available, and each cancelled quote is refunded for exactly the pairs it did not deliver (see 3.8).
The workflow: request quotes → accept them as a batch → watch progress in the dashboard → one email when everything is done → download. One payment, one notification, every file in reach.
Everything in this document is in production today:
| Endpoint | What It Does |
|---|---|
POST /translate/quote | Price one or more documents into one or more languages |
GET /translate/quote/{quote_id} | Look up one quote |
GET /translate/quotes | Your 30 most recent quotes |
POST /translate/accept/{quote_id} | Pay and start one quote |
POST /translate/accept-batch | Pay and start up to 9 quotes together |
GET /translate/download/{job_id} | Download links for a finished job |
POST /translate/cancel/{quote_id} | Cancel a running job and refund every undelivered pair |
GET /translate/status/{job_id} | Live status of one job: every pair, the ETA and any refund |
GET /translate/task/{task_id} | Live status of a whole batch |
POST /translate/retry/{quote_id} | Retry the failed pairs, at no charge |
POST /translate/refund-failed/{quote_id} | Refund the failed pairs now instead of retrying |
GET /translate/models | The six agents (public) |
GET /translate/languages | The live language catalog and limits (public) |
Progress, history, retries, brand terms and billing live in the Account Dashboard. The full contract for every endpoint, including each error, is in the API Reference §15.
| Risk | Impact | Mitigation |
|---|---|---|
| Very large documents | Unreliable splitting, runaway cost | 300 KB per document, checked at quote time before any charge and again when the quote is accepted; a larger document is refused, never cut short |
| Floods of submissions | Queue congestion, cost | A valid API key on every request, payment before any work starts, at most 12 languages and 42 documents per quote with one worker per language, and an engine-wide $600 daily cap on AI spend that is checked before any charge |
| Unsupported languages | Charged for a translation we cannot do | Every language code is checked against the live catalog before a quote is priced |
| Paying for undelivered work | Customer charged for nothing | Failed pairs are retried free and refunded automatically; a paid job that can't start is refunded in full; a pair is never both delivered and refunded |
| A document changes after the quote | Charged a price computed on other content | Every document is fetched again and compared before the charge; a changed price is re-quoted, never charged |
| AI provider interruptions | Jobs slow down or fail | Automatic retries; a pair that still fails is reported, never delivered damaged, retried free on request, and refunded automatically if it can't be delivered |
| Damaged output | Broken documentation | Protected content never reaches the agent, and seven validators check every section before delivery |
| Customer data privacy | Trust | A private folder per quote, signed download links that expire after 7 days, and automatic deletion after 93 days |
| Cost underestimation | Negative margin | Flat per-pair pricing, with Claude Opus 4.8 priced from its actual cost on large documents |
| Complex scripts | Arabic, Urdu and Hindi take longer | Per-language tuning of section size and difficulty, with every language running in its own container so a slow one never delays the rest |
What we offer: AI-powered document translation that actually works for technical content.
Why it works:
Revenue potential: every document translated into 11 languages is 33.00. Ten customers translating ten documents a month into 11 languages is 1,100 pairs, or 3,300.00 a month, against an AI cost of a few cents per pair.
Bottom line: we solved this problem for ourselves. Now we can solve it for others — and let the service help fund the ministry's mission.