URL as the key
Translations are addressed the way your API already is: endpoint URL + language, scoped to your project (via API key).
There is no separate “translation resource id.” After normalization (sorted query params; tracking params stripped), ?id=1 and ?id=2 are different keys.
Trial vs live keys
Hot path is the same: Authorization: Bearer ml_… → project. GET never counts as translation work and never uses paid meters.
Ensure vs get
Miss (ensure creates or updates) means MT work ran — that is the expensive path. A cache hit returns stored locale without MT. GET is always a pure read. Missing or expired GET → 404; fall back to source.
Path selectivity
Only human-facing string leaves are translated. Keys, numbers, booleans, and nulls stay put.
- Project denylist always applies
- Per-request
skip_pathsadds to it (e.g.sku,items.*.id) - Optional
translate_pathsallowlists eligible paths
Syntax: dot paths with * for arrays.
TTL
Free / unbilled records are ephemeral by default (24h). Ensure refreshes expiry (including cache hits). GET does not extend life. Expired rows behave as missing.
Paid / durable policy (when billing is active) keeps writes around until source change or DELETE. Character meters follow the Stripe billing period; stored endpoints count current source URLs (DELETE frees a slot). See Pricing (Starter / Growth / Scale; Checkout defaults to Growth).
If billing lapses, live keys keep working with 24h TTL and a small fuse (20 endpoints / 20k chars) — not a wipe, and not an unbounded translation bill.
Sibling invalidation
When ensure changes the source fingerprint for a URL, other locales for that URL are dropped (DB + cache). They are not served stale — ensure each language again when you need it. Only changed string leaves are retranslated.
Languages
Every code on the language list is available. There is no pack to unlock. Pass lang (and optional source_lang) as those codes.