# Translate text into Thai

The translation endpoint turns text in any language into natural Thai. Control register, loanwords, and terminology with glossaries, document context, and adaptive style examples.

[POST /v1/translate](https://paxalabs.com/docs/api/translate) translates text into Thai from any source language. Send one string or an array of segments with the model id `paxa-translation-lite-v1`; the response carries one Thai translation per segment with the detected source language. Like every endpoint, the request is charged before inference and refunded automatically when inference fails ([Credits](https://paxalabs.com/docs/credits)).

curl:

```bash
# --max-time covers a large request; curl defaults to no limit,
# but most clients and SDKs time out well before a big translation ends.
curl -X POST https://api.paxalabs.com/v1/translate \
  --max-time 300 \
  -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"
  }'
```

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
  }
}
```

## Billing

Translation bills at two rates. Characters you send in `text` are translated, and cost 25 credits per 1,000. Characters in `instructions`, `context`, `examples`, `do_not_translate`, and `glossary` are read but never translated, and cost 8 credits per 1,000, counting both sides of every pair. The two are summed and rounded up once to the next hundredth of a credit, and every request costs at least 2 credits.

Reference material costs less to serve. It becomes input the model reads. Translated characters drive everything the model writes as well. Send the context a passage needs. Every response reports the exact split in `usage`. A charge is always reconcilable against the body you sent.

Two ceilings apply. The `text` field accepts at most 20,000 characters, because it is the only field that produces output. That allowance is counted once per requested rendering, since each rendering produces that much Thai. Longer text answers [400 text_too_long](https://paxalabs.com/docs/errors#text_too_long). The request as a whole accepts at most 200,000 billable characters across every field, and beyond that answers [400 request_too_large](https://paxalabs.com/docs/errors#request_too_large). Neither charges anything. Both ceilings are measured after unused glossary terms are dropped. Attaching a large glossary never pushes a request over one.

## Batch segments

An array of up to 200 segments translates in one request. The segments share document context, which keeps pronouns, register, and terminology consistent across them. The per-request minimum applies once for the whole array. Segment boundaries come back exactly as sent. Batch UI strings, subtitles, and product feeds.

Batching also keeps `context` and `glossary` cheap. Both are sent once for the whole request. The same context costs the same on one segment as on 200. Spread across a full batch it is a fraction of a percent of the bill.

Batching:

```typescript
// One request, one charge: segments share document context and the
// per-request minimum applies once.
const strings = ["Sign in", "Create account", "Forgot password?"];

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: strings, model: "paxa-translation-lite-v1" }),
});

const { translations } = await response.json();
for (const [index, item] of translations.entries()) {
  console.log(`${strings[index]} -> ${item.text}`);
}
```

## Keyed segments

A segment can be an object as well as a string. Give it an `id` and the result carries that id back. Your code then matches results to inputs by key, with no dependence on position. Ids are never interpreted and need not be unique. Order is preserved either way, and one array may mix objects and plain strings.

A segment object also takes `max_length`, the longest acceptable translation in characters. The model compresses the translation to fit and does not truncate it. A value below what the meaning needs therefore costs fidelity. A result that still exceeds the limit is delivered and flagged. Use it for subtitles and UI strings, where the space is fixed.

TypeScript:

```typescript
// Key the segments and you match results by id, not by position.
// max_length compresses to fit; a result that still exceeds it comes
// back delivered and flagged rather than refused.
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({
    model: "paxa-translation-lite-v1",
    text: [
      { id: "cta.save", text: "Save changes", max_length: 24 },
      { id: "cta.cancel", text: "Cancel", max_length: 12 },
      "A plain string works alongside them.",
    ],
    do_not_translate: ["Paxa TTS Flash"],
  }),
  signal: AbortSignal.timeout(300_000),
});

const { translations } = await response.json();
for (const segment of translations) {
  if (segment.review) {
    console.warn(segment.id, segment.review.reason, segment.review.detail);
  }
  console.log(segment.id ?? "(unkeyed)", segment.text);
}
```

## Markdown and HTML

Set `format` to `"markdown"` or `"html"` and the model translates only the human-readable prose. Link targets, code, tags, and attributes come back untouched. Thai is written without spaces between words. A translator that shifts an inline tag by one character therefore moves where a word breaks for a Thai reader. That is the reason these modes exist.

## Review flags

Every delivered segment is checked against what the request asked for: nothing empty, nothing left untranslated, the terminology and preserved strings present, `max_length` respected, the markup structurally intact. A segment that fails a check carries a `review` object naming the reason.

A flag never refuses a request. The translation is delivered and charged either way, and a flag means the segment is worth a look. Checks that could reject a delivery would be a new way to lose a request you already paid for. A response whose segments carry no `review` member passed every check.

## Request options

| Field | Values | Effect |
| --- | --- | --- |
| source | auto (default), en, zh, ja, ko, ar, fr, de, hi, id, ms, pt, ru, es, vi | Pin the source language, or let the model detect it per segment. Auto-detection accepts any language, including mixed-language text. |
| formality | auto, formal, casual | Polite written Thai with polite particles where natural, conversational Thai, or Thai matching the source's tone. |
| borrowed_words | auto, transliterate, preserve | The established Thai form where one exists, Thai script throughout, or the original script for technical terms and names. |
| format | text, markdown, html | What the segments are written in. The markup modes preserve every construct exactly and translate only prose. |
| alternatives | 1 to 5 | How many renderings to return per segment, best first. Each one is a full pass over the text and bills as one. |
| do_not_translate | up to 100 strings | Strings that must appear in the translation exactly as written. Billed at the reference rate. |
| instructions | up to 4,000 chars | Free-form direction that the fixed options cannot express, such as house rules, an audience, or a term to avoid. Applied to every segment. Billed at the reference rate. |
| context | up to 100,000 chars | Background the model reads but never translates: the surrounding document, domain, or audience. Billed at the reference rate. |
| examples | up to 100 pairs | Source and target pairs whose tone and phrasing the translation imitates. Billed at the reference rate. |
| glossary | up to 5,000 pairs | Terminology to enforce wherever a source term appears. Terms that cannot fire on this text are dropped before the request runs. Dropped terms are neither sent nor billed. Billed at the reference rate. |
| glossary_mode | matched, all | Whether to drop glossary terms that do not appear in the text. "matched" is the default; "all" sends and bills every term you attach. |

### Instructions

`formality` and `borrowed_words` cover the common cases. `instructions` covers the rest. Write plain direction, in English or Thai. It applies to every segment of the request. Say who the reader is, which house rules apply, which words to avoid, or how a recurring phrase should land.

Instructions govern style. They cannot change the shape of the response. They cannot make the model act on the text it is translating either. A segment that reads like an order is still translated as text. Use `context` for background the model should know. Use `instructions` for what it should do.

curl:

```bash
curl -X POST https://api.paxalabs.com/v1/translate \
  --max-time 300 \
  -H "Authorization: Bearer $PAXA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Your table is ready. Please follow me.",
    "model": "paxa-translation-lite-v1",
    "instructions": "Hotel front desk speaking to a guest. Warm, never stiff. Keep room and restaurant names in English."
  }'
```

### Glossary

Glossary terms are applied contextually. The model inflects, cases, and places each term grammatically, and matches it even when the source text uses a variant form. Literal substitution is often ungrammatical in Thai, which is why glossaries work this way. If a term must appear verbatim, for example a product name or a legal phrase, state that in `context`.

Send your whole glossary. Terms whose source does not appear in the text cannot change the translation. Those terms are dropped before the request runs. They are never sent to the model and never billed. A 500-term style guide attached to a short support reply costs what its two or three firing terms cost. The response reports `glossary_terms_sent` and `glossary_terms_available`. Both figures show how a request landed.

Matching ignores case, accents, and typographic apostrophes. A glossary typed as `customer's` matches a document typeset with `customer’s`, and `hotel` matches `hôtel`. Matching also sees through suffixes and German compounds: `invoice` fires on `invoices`, and `Rechnung` fires on `Rechnungsnummer`. It needs no word boundaries. It works the same in Chinese, Japanese, and Thai.

Matching cannot see through inflection that changes the stem of a word. An irregular plural and a Russian case ending are both examples. Set `always: true` on such a term to send it regardless. Use the same flag for a term that governs a concept, such as an abbreviation the text spells out. Setting `glossary_mode` to `"all"` sends and bills every term you attach.

### Register

Thai grammar encodes politeness. Particle choice, pronouns, and vocabulary all shift with register. `formality: "formal"` produces polite written Thai suited to announcements, support replies, and official copy. `"casual"` produces the conversational Thai of chat and social captions. `"auto"`, the default, mirrors the source tone. Rendered register pairs and full translated specimens are on the [translation page](https://paxalabs.com/translation).

### Borrowed words

Thai absorbs foreign terms as established loanwords (computer is คอมพิวเตอร์, not a coined Thai word). `"auto"` uses the established form where one exists and keeps unestablished terms in their original script; `"transliterate"` renders borrowed words in Thai script throughout; `"preserve"` keeps technical terms and proper nouns exactly as written, useful for developer documentation and brand copy.

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": "ส่งฟรี"}]
  }'
```

## Source language

Each response entry reports `detected_source`, the ISO 639-1 code of that segment's input (or "und" when inconclusive). Mixed-language input is handled per segment, and text that is already Thai passes through unchanged apart from register adjustments; it bills normally.

## Latency and retries

Translation is a reasoning workload. Expect a response in seconds, and up to tens of seconds near the character ceiling. Set your HTTP client's timeout to at least 300 seconds. Most clients and SDKs default to far less than a large request needs. A client-side timeout aborts a request that was already charged. Send an [Idempotency-Key](https://paxalabs.com/docs/idempotency) to make retries safe. The model is not deterministic. A replayed request can word its translation differently from the original delivery, and the charge is never repeated either way.

A request that runs long enough writes its response headers before the outcome is known. Intermediate proxies abandon a connection that has been silent too long, and the early headers prevent that. Such a response always has status 200. It reports a failure in the body, as the same problem document a failed request returns: a `title` and a `status` where `translations` would be. If your client sends requests near the character ceiling, treat a body carrying `title` as the error it describes. Shorter requests always carry their status on the status line.

> Content declined by the upstream safety system answers [422 content_blocked](https://paxalabs.com/docs/errors#content_blocked) with the charge refunded. Retrying the same content fails the same way; revise the input.

- [Translate text](https://paxalabs.com/docs/api/translate): Full request and response reference
- [Credits, charges, and refunds](https://paxalabs.com/docs/credits): How credits and refunds work
- [Make retries safe with idempotency keys](https://paxalabs.com/docs/idempotency): Safe retries with Idempotency-Key
- [Error codes and responses](https://paxalabs.com/docs/errors): Every error code
