DocsHTTP API6 min read

HTTP API

Four endpoints, JSON in and JSON out. Every SDK is a thin wrapper over this, so anything a framework cannot do you can do yourself.

POST /v1/translate
One string. The one you will call most.
POST /v1/translate/batch
Up to 20 strings, in one language or up to eight, answered by index.
GET /v1/strings?target=ar
The whole project as one map, by source text or by your own key.
GET /v1/languages
What the project is configured to speak.
GET /health
No key. Reports which backends are wired, never a secret.
curl
# $SIYAK_HOST is the host printed on your project's API keys screen.
curl -s "$SIYAK_HOST/v1/translate" \
  -H "authorization: Bearer $SIYAK_KEY" \
  -H "content-type: application/json" \
  -d '{"text":"Your card expires soon","target":"ar","context":"screen:checkout"}'

# → {"text":"بطاقتك تنتهي قريبًا","origin":"model","status":"pending",
#    "chars":20,"badge":"Translated with Siyak",
#    "usage":{"used":1320,"limit":15000,"month":"2026-10"}}

The alternative header x-siyak-key works too; nothing else about the request changes.

Request fields

fieldrequirednotes
textyes1–500 characters after sanitizing: control characters stripped, ends trimmed. Longer is a 400, not a truncation.
targetyesBCP-47, e.g. ar, ar-EG, zh-Hans. Stored in one canonical spelling: AR-eg becomes ar-EG, and both name the same string.
targets[]batchUp to 8 tags instead of one target. Each language bills its own characters.
sourcenoFalls back to the project's configured source language.
contextno≤160 characters, no < or >. Part of the stored identity — see Concepts.
keynoYour own name for the string, e.g. checkout.title. Dotted, ≤80 characters. Optional, and never required to translate.
items[]batch1–20 entries of {text, context?, key?}. The language envelope applies to all of them.

Response fields

fieldmeaning
textThe translation.
targetThe language you asked for, in its canonical spelling. Match on this, never on what you sent.
origincache | approved | pending | model — where this answer came from.
statusapproved | pending | rejected — whether anyone has signed off on it.
charsWhat this call billed, when it billed.
badgePresent when attribution is required. Render it — the free tier is paid for with it.
usage{used, limit, month} — the project's character meter.
resultsIn a targets[] batch only: {tag: {output, origin, status}}, one entry per language, the tags canonicalised.
glossary_driftSet on a model answer that ignored a pinned term.

Batch semantics

  • Results are matched by index, never by the echoed input — the server sanitizes before echoing, so the text you sent is not always the text it returns.
  • A row with output: null carries its own error string; the envelope is still 200.
  • If the month runs out mid-batch the server stops there: fewer rows than you sent means nothing further was attempted, and the remaining items stay untranslated rather than charged.
  • With targets[] the answers move to results, keyed by the canonical tag — the targets array in the reply tells you which spelling was used. output stays null so a row never changes shape.

Status codes

codeerrorwhat to do
400invalid_requesttext too long, bad target tag, context with angle brackets. Fix the call; it is deterministic.
401invalid_api_keyRevoked, disabled project, wrong environment, or a typo — deliberately indistinguishable so the endpoint cannot be probed for live keys.
402quota_exceededThe month is spent. usage is in the body. Stop, tell the developer, and do not retry in a loop.
403origin_not_allowedYour browser origin is not on the project's allowed-domain list. Add it in the dashboard.
404not_foundWrong path. Everything lives under /v1.
409key_conflictThe key you sent already names a different string in that language. Nothing was stored and nothing was billed — rename your key or drop it, and the call is a plain translate again.
429rate_limitedHonor retry_after. The limit is per key, per minute.
503backend_unavailableThe model is down. The string was still filed in the Inbox, so someone can answer it by hand — serve your source text and move on.
500internal_errorOurs. The message is intentionally empty; open a ticket with the month and project.