Translate API
Organization translation for plain text and HTML. Powered by Microsoft Azure AI. Send one request — large documents are chunked on the server, then joined back into a single translation.
Overview
POST /api/v1/translate translates a string from one language to another. The public homepage translator and the organization Playground both call this endpoint. With an API key, the request uses your organization model, character limits, translation tone, and per-language localization notes. Without a key, the request uses the global service settings and is not counted against a monthly quota.
- Text and HTML. HTML tags, classes, and ids stay in place; only visible copy is translated.
- Automatic chunking when the source is longer than 75% of the input character limit.
- Entity protection (on by default) keeps URLs, emails, and HTML attributes out of the model.
- Usage is recorded only for approved, active organizations.
Authentication
Send the organization API key in either header. The key is issued when an organization is registered and approved.
x-api-key: YOUR_API_KEY
Authorization: Bearer YOUR_API_KEYThe key must belong to an approved, active organization, and translation must be enabled for that organization. A missing key is treated as public access. An unknown, unapproved, or inactive key returns 403.
Translate API
GET /api/v1/translate returns a short JSON description of the endpoint. It does not translate. The model, web search, and reasoning effort are chosen in the admin console for the organization. They are not request fields.
Request body
| Field | Required | Description |
|---|---|---|
| text | Yes | Source string. Plain text or HTML, depending on contentType. |
| fromLanguage | Yes | Source language code that is enabled for the service, for example en. |
| toLanguage | Yes | Target language code. Localization notes for this code are applied when set. |
| contentType | No | text (default) or html. Must be in the allowed content types. |
| protectEntities | No | Default true. Send false to translate URLs, emails, and attributes as normal text. |
Response
A successful call returns HTTP 200. usage and organizationare included only when the request used a valid API key.
{
"success": true,
"data": {
"originalText": "Hello, world!",
"translatedText": "¡Hola, mundo!",
"fromLanguage": "en",
"toLanguage": "es",
"contentType": "text",
"translatedAt": "2026-10-19T12:00:00.000Z",
"protection": {
"enabled": true,
"protectedCount": 1,
"restoredCount": 1,
"savedCharacters": 12,
"missingPlaceholders": 0
}
},
"usage": { "current": 12, "limit": 10000, "remaining": 9988 },
"organization": { "id": "org_id", "name": "Example" },
"performance": { "charactersProcessed": 13 }
}Languages
Send the language code, the same value the public translator uses. A code is accepted only when an administrator has enabled it. If the code is not in that list, the API returns 400 with the unsupported language name in the error. Targets ko, zh, and ja (and the English names Korean, Chinese, Japanese) receive a fluency note and a higher output-token budget.
Text and HTML
contentType: "text" returns only the translation. Custom tone replaces the default “translation only” instruction when the organization has saved one.
contentType: "html" keeps tags, attributes, classes, and ids. Only text between tags is translated. Markdown fences such as ```html are stripped from the model output. After translation, the HTML is trimmed and extra blank lines are collapsed.
{
"text": "<h1>Pricing</h1><p>Plans start at <a href=\"/pricing\">this page</a>.</p>",
"fromLanguage": "en",
"toLanguage": "fr",
"contentType": "html"
}Chunking
You do not send chunks. The server splits a long source, translates each piece, then joins the results into one translatedText. Chunking starts when the original text length is greater than 75% of the input character limit for that request. With the fallback limit of 50,000 characters, that threshold is 37,500. Shorter texts are one model call.
Chunk size
- Direct OpenAI models: 1,200 characters.
- Models with a reasoning-effort control: 2,000 characters.
- Other models, including direct Mistral: 3,000 characters.
Plain text
The source is split on sentence endings (. ! ? followed by whitespace). Sentences are packed into the current chunk until the next sentence would pass the chunk size. The chunk is then closed and a new one starts. There is no overlap between chunks. Translated chunks are trimmed and joined with a single space.
HTML
The walker reads the document tag by tag and tracks nesting depth. A chunk closes only after a closing block tag when depth returns to zero and the chunk would otherwise exceed the size. Block tags are div, section, article, main, header, footer, nav, aside, p, and h1–h6. The closing tag stays with the chunk so the fragment is complete. If that still leaves a single piece larger than the limit, the fallback splits on sentence punctuation. HTML results are joined with newlines, extra blank lines are removed, and the combined markup is cleaned before it is returned.
How batches run
- Direct Mistral: up to 3 chunks in parallel.
- OfoxAI, direct OpenAI, and reasoning models: one chunk at a time, so rate limits and format errors stay isolated.
- Direct OpenAI waits 3 seconds between chunks so the token window can recover.
- Entity placeholders are assigned on the full document before the split, so the same URL or email keeps one token in every chunk. They are restored after the pieces are joined.
- If any chunk fails, the whole request fails with HTTP 500 and the message from that chunk. Nothing partial is returned.
Each chunk gets its own output-token budget from the model catalog, including the per-model cap (for example GLM Flash at 4,096). Asian targets use a larger token multiplier on single-shot calls. Set a timeout of at least 120 seconds when the source is near the character limit.
Entity protection
Before the model runs, non-translatable spans are replaced with tokens such as ⟦X001⟧. The model is instructed to copy those tokens exactly. After translation they are swapped back to the original values. This keeps links, class names, and contact details stable and reduces tokens.
- Plain text: emails, URLs, phone-like numbers.
- HTML: those spans, plus attributes such as href, src, srcset, class, id, style, action, and data-url.
- The same original value always maps to the same placeholder, including across chunks.
- Disable with
"protectEntities": false. Valuesfalse,"false",0, and"0"all turn it off.
data.protection reports how many spans were mapped, how many were restored, characters saved, and how many placeholders the model dropped (missingPlaceholders). A non-zero missing count means a URL or attribute could not be put back.
Tone and localization
These are organization settings, not body fields. Translation tone (Console → API Keys) is appended to every authenticated request. When it is empty, text requests use the default: provide only the translation, with no explanation.
Localization (Console → Localization) stores a note per language code. The note for toLanguage is added to the prompt for that request only. HTML requests receive both the tone and the localization note inside the HTML rules.
Limits and errors
Input and output character limits, enabled languages, allowed content types, and the monthly request cap come from the organization when a key is present, otherwise from the global translation settings. A monthly cap applies only to authenticated calls.
| Status | When |
|---|---|
| 400 | Missing text, fromLanguage, or toLanguage; text over the input limit; language or content type not enabled; output over the output limit. |
| 403 | Unknown, unapproved, or inactive API key. |
| 429 | Monthly request limit reached. Body includes currentUsage and limit. |
| 500 | A chunk failed, or the model returned an empty translation. |
| 503 | Translation is disabled for the organization, or the model call failed on a single-shot request. |
Code examples
Replace YOUR_API_KEY and the host. The same JSON body works in every language. Node sample below sends HTML and uses the Bearer header; the others send text with x-api-key.
curl -X POST "http://localhost:3000/api/v1/translate" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"text": "Hello, world! Visit https://example.com",
"fromLanguage": "en",
"toLanguage": "es",
"contentType": "text",
"protectEntities": true
}'Organization console
- Dashboard — workspace status after login at
/dashboard. - Playground — run a live translation with the organization key.
- API Keys — copy or regenerate the key, read a short request sample, and save translation tone (up to 50,000 characters).
- Localization — instructions stored per language code and applied when that code is
toLanguage. - Usage — cached input/output characters, request counts, and cost from the organization per-million rates. Refresh is limited to once every 10 minutes.
- Payments — API credit balance. Buy Credit records a pending purchase when you continue to checkout. An administrator adds or subtracts balance; the balance can be negative.
- Settings — organization profile fields the account is allowed to edit.
Administrators set the model (including OfoxAI models such as GPT-6 Luna), character limits, web search, reasoning effort, and credit balance. Those controls are not part of the public translate body.
Synth Detector
POST /api/synth_v1/analyze screens plain text or HTML for AI-like writing patterns. It does not use the translation API key. The body is { "content": "...", "sanitize_ai_marks": false }. Content is limited to 5,000 characters. The result is stylometric screening, not proof of authorship. The same screen is available at /synthdetector.
curl -X POST "http://localhost:3000/api/synth_v1/analyze" \
-H "Content-Type: application/json" \
-d '{ "content": "Paste the passage to screen." }'