# Translate text

`POST https://api.paxalabs.com/v1/translate`

POST /v1/translate turns text in any language into Thai, one translation per input segment. Charged in credits per character before inference, refunded on failure.

Send `text` as one string or an array of segments. The [Translation guide](https://paxalabs.com/docs/translation) covers register, loanwords, glossaries, and style.

## Headers

- `authorization` (Bearer pxa_..., required): Authorization: Bearer pxa_... (recommended).
- `x-api-key` (pxa_..., optional): Alternative to the Authorization header. Ignored when Authorization is present.
- `idempotency-key` (string, optional, 1 to 200 characters, pattern ^[!-~]+$): Makes a retry safe: two requests carrying the same key are charged once. One request per key runs at a time. A concurrent duplicate answers 409 idempotency_in_flight. Reuse a key only to retry an identical request. A changed payload is rejected with 422. Accepts up to 200 printable ASCII characters other than space.

See authentication at https://paxalabs.com/docs/authentication.

## Body

- `text` (string or object or array of string or objects, required, example "The quick brown fox jumps over the lazy dog."): The text to translate: one string, or an array of up to 200 segments translated together (one request, one charge, shared document context; segment boundaries are preserved in the response). Translated characters cost 25 credits per 1000. Reference characters, meaning instructions, context, examples, do_not_translate strings, and glossary, cost 8 credits per 1000 because the model reads them without translating them. Both are billed per character, summed, and rounded up once to the next hundredth of a credit, minimum 2 credits per request. This field accepts at most 20,000 characters, counted once per requested rendering, because each rendering produces that much Thai. The whole request accepts at most 200,000 billable characters. The response reports the exact split in usage.
- `model` (string, required, 1 to 100 characters, example "paxa-translation-lite-v1"): Translation model id, for example paxa-translation-lite-v1. GET /v1/models lists the served catalog.
- `source` (string, optional, default "auto", up to 16 characters): Source language as an ISO 639-1 code, or "auto" (the default) to detect it per segment. Codes accepted as explicit pins today: en, zh, ja, ko, ar, fr, de, hi, id, ms, pt, ru, es, vi. Auto-detection accepts any input language, including mixed-language text.
- `target` ("th", optional, default "th"): Target language. Thai is the only target served today; the field exists so more targets can arrive without a breaking change.
- `formality` ("auto" or "formal" or "casual", optional, default "auto"): Thai register: "formal" produces polite written Thai (polite particles where natural, formal vocabulary), "casual" produces conversational Thai, and "auto" (the default) matches the tone of the source.
- `borrowed_words` ("auto" or "transliterate" or "preserve", optional, default "auto"): How to handle borrowed words. "auto", the default, uses the established Thai form where one exists. "transliterate" renders borrowed words in Thai script. "preserve" keeps technical terms and proper nouns in their original script.
- `format` ("text" or "markdown" or "html", optional, default "text"): What the segments are written in. "text" (the default) treats them as prose. "markdown" and "html" translate only the human-readable prose. Link targets, code, tags, and attributes come back untouched. Thai is written without spaces between words. An inline tag moved by one character therefore changes where a word breaks, which is why these modes exist.
- `alternatives` (integer, optional, default 1, min 1, max 5): How many distinct renderings to return per segment, best first. Above 1 the extras arrive in the alternatives array of each result. Each rendering is a full pass over the text. The text rate therefore multiplies by this number, and so does the text ceiling: asking for 5 renderings leaves a 5th of the character allowance for the text itself. Reference material is read once and charged once. At most 5.
- `do_not_translate` (array of strings, optional): Strings that must appear in the translation exactly as written: product names, code identifiers, citations. Applied on top of borrowed_words, which decides everything else.
- `instructions` (string, optional, 1 to 4,000 characters, example "Write for a hotel front desk. Keep room names in English."): Free-form direction for the translator, covering tone, register, terminology, house rules, and anything the fixed options cannot express. Applied to every segment. Its characters bill at the reference rate. At most 4,000 characters. Instructions govern style only. They cannot change the response shape. They cannot make the model act on the text it is translating either.
- `context` (string, optional, 1 to 100,000 characters): Background that conditions the translation without being translated: the surrounding document, the domain, or the intended audience. Its characters bill at the reference rate. At most 100,000 characters.
- `examples` (array of objects, optional): Style examples the translation imitates in tone, register, and phrasing. Accepts up to 100 source and target pairs. Both sides of every pair bill at the reference rate.
  - `examples[].source` (string, required, 1 to 2,000 characters): A source sentence in the style to follow.
  - `examples[].target` (string, required, 1 to 2,000 characters): Its preferred Thai translation.
- `glossary` (array of objects, optional): Terminology the translation must use. Accepts up to 5,000 term pairs. The model applies each term contextually, inflecting, casing, and placing it grammatically. Send a whole glossary. Terms whose source does not appear in the text are dropped before the request runs. Dropped terms are neither sent to the model nor billed. Matching ignores case, accents, and typographic apostrophes. It sees through suffixes and German compounds. It needs no word boundaries, and works in Chinese, Japanese, and Thai. Both sides of every surviving pair bill at the reference rate. The response reports how many were sent.
  - `glossary[].source` (string, required, 1 to 200 characters): The term as it appears in the source text.
  - `glossary[].target` (string, required, 1 to 200 characters): The Thai rendering to use for it.
  - `glossary[].always` (boolean, optional, default false): Send this term even when it does not appear in the text. Use it for a term that governs a concept, such as an abbreviation the text spells out. Use it also when inflection changes the stem of the word, as an irregular plural or a Russian case ending does.
- `glossary_mode` ("matched" or "all", optional, default "matched"): How to handle the glossary. "matched", the default, sends and bills only the terms whose source appears in the text. "all" sends and bills every term you attach, for callers who would rather pay than reason about matching.

## Response

- `translations` (array of objects, required): One entry per input segment in input order; exactly one entry when text was a single string.
  - `translations[].id` (string, optional): The id you gave this segment, echoed back. Absent when the segment was sent as a bare string.
  - `translations[].text` (string, required): The Thai translation of this segment.
  - `translations[].detected_source` (string, required): The detected (or explicitly pinned) source language of this segment as an ISO 639-1 code; "und" when detection is inconclusive.
  - `translations[].alternatives` (array of strings, optional): Further renderings of this segment, best first, when alternatives was above 1. The primary rendering stays in text and is not repeated here.
  - `translations[].review` (object, optional): Present only when a delivered translation is worth checking. These are our own checks on the result. They never fail or refuse a request, and the translation is delivered and charged either way. A response whose segments carry no review member passed every check.
    - `translations[].review.reason` (string, required): Why this segment is worth a look. `empty`, the model returned nothing. `untranslated`, the output matches the input. `glossary_term_missing`, a term you sent does not appear. `preserved_span_missing`, a do_not_translate string is absent. `too_long`, the result exceeds max_length. `markup_changed`, the markup structure differs.
    - `translations[].review.detail` (string, optional): The specific term, span, or measurement behind the reason, when there is one.
- `usage` (object, required): What the request was billed for.
  - `usage.text_chars` (integer, required): Characters of text, summed across segments.
  - `usage.reference_chars` (integer, required): Characters the model read without translating: instructions, context, examples, do_not_translate strings, and the glossary terms actually sent.
  - `usage.billable_chars` (integer, required): text_chars plus reference_chars.
  - `usage.credits` (number, required): What this delivery cost, in credits, exact to a hundredth. An idempotent replay reports the ORIGINAL request's charge, since that one charge is what paid for this delivery too; your balance moves only once.
  - `usage.glossary_terms_sent` (integer, required): Glossary terms that reached the model and were billed for.
  - `usage.glossary_terms_available` (integer, required): Glossary terms attached to the request. The gap between this and glossary_terms_sent is what pruning dropped.

## Errors

- `validation` (400): The request body, query, or headers failed validation against the endpoint schema, or broke a rule the endpoint's reference states. A field the schema refused adds two fields to the problem body. path names the first failing field by location and name in dot form, such as body.voice or headers.idempotency-key. A body that could not be read gives the path body alone. reason is one stable code from the validation reasons list.
- `unknown_model` (400): The model field does not name a served model.
- `unsupported_language` (400): The source field names a language the model does not accept as an explicit pin.
- `text_too_long` (400): The text exceeds the model's per-request character ceiling.
- `request_too_large` (400): The request's billable characters exceed the model's total ceiling. Translation bills every field: instructions, context, examples, do_not_translate strings, and the glossary count toward the ceiling alongside the text.
- `unauthorized` (401): The request carried no API key, or the key is invalid or disabled.
- `insufficient_credits` (402): The account does not have enough credits for this request. Nothing was charged.
- `key_limit` (403): This API key reached its spending cap. Nothing was charged.
- `idempotency_in_flight` (409): Another request with this Idempotency-Key is in flight right now.
- `idempotency_refunded` (409): The original request under this Idempotency-Key failed and was refunded.
- `idempotency_mismatch` (422): This Idempotency-Key was already used for a different request.
- `content_blocked` (422): The upstream safety system declined to process this content. The charge was refunded.
- `rate_limited` (429): Requests per minute for the plan are exhausted. One window covers the whole account, across every product and every key.
- `concurrency_limited` (429): The account holds the plan's full count of concurrent requests for this product. Nothing was charged. Each product is limited separately, and an open live connection holds one slot of its product for its lifetime.
- `internal` (500): Request state was inconsistent on the server.
- `provider_error` (502): Model inference failed after the request was charged.
- `provider_unavailable` (503): The model behind this endpoint is not available right now. Nothing was charged.

## Example

curl:

```bash
curl -X POST https://api.paxalabs.com/v1/translate \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Welcome to our store. Free shipping on orders over 500 baht.",
    "model": "paxa-translation-lite-v1",
    "source": "en",
    "formality": "formal",
    "borrowed_words": "auto",
    "context": "Homepage banner for an online fashion store.",
    "glossary": [{"source": "free shipping", "target": "ส่งฟรี"}]
  }'
```

TypeScript:

```typescript
const response = await fetch("https://api.paxalabs.com/v1/translate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.PAXA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Welcome to our store. Free shipping on orders over 500 baht.",
    model: "paxa-translation-lite-v1",
    formality: "formal",
  }),
  // A large request can run for minutes; give it room.
  signal: AbortSignal.timeout(300_000),
});

if (!response.ok) throw new Error(`Translation failed: ${response.status}`);
const { translations, usage } = await response.json();
console.log(translations[0].text, `${usage.credits} credits`);
```

Python:

```python
import os
import requests

response = requests.post(
    "https://api.paxalabs.com/v1/translate",
    headers={"Authorization": f"Bearer {os.environ['PAXA_API_KEY']}"},
    json={
        # Send the whole glossary. Terms that do not appear in the text
        # are dropped before the request runs, and never billed.
        "glossary": [
            {"source": "free shipping", "target": "ส่งฟรี"},
            {"source": "cash on delivery", "target": "เก็บเงินปลายทาง"},
        ],
        "text": "Welcome to our store. Free shipping on orders over 500 baht.",
        "model": "paxa-translation-lite-v1",
    },
    # A large request can run for minutes; requests defaults to waiting
    # forever, but most deployments put a shorter limit in front of it.
    timeout=300,
)
response.raise_for_status()

body = response.json()
print(body["translations"][0]["text"])
print(body["usage"]["glossary_terms_sent"], "of", body["usage"]["glossary_terms_available"])
```

Response:

```json
{
  "translations": [
    {
      "text": "ยินดีต้อนรับสู่ร้านของเรา ส่งฟรีเมื่อสั่งซื้อครบ 500 บาท",
      "detected_source": "en"
    }
  ],
  "usage": {
    "text_chars": 60,
    "reference_chars": 0,
    "billable_chars": 60,
    "credits": 2,
    "glossary_terms_sent": 0,
    "glossary_terms_available": 0
  }
}
```
