The call works in development. Now you are writing the part that runs in production – the catch block. An HTTP error from a third-party API is opaque on its own: a red status code, and no obvious answer to the only question that matters at 3am – is this my request, my key, my plan, or their servers? And of these, which ones do I retry, and which do I surface to the user?
Lingo.dev answers that with structure. Every error – from every endpoint, sync or async – comes back as the same JSON object with the same status code drawn from one fixed table. The status code is not a label, it is an instruction: it tells you whether to fix the request, rotate the key, top up the account, back off, or retry. Read the code, know the next move. One error handler, keyed on the status, covers the whole API.
On this page
- The error shape
- Status codes
- Which errors to retry
- 402 vs 429: two different limits
- Where async job errors live
The error shape#
Every non-2xx response has the same body: a JSON object with a single message field describing what went wrong.
{
"message": "Invalid API key"
}That is the whole contract. There is no envelope to unwrap, no per-endpoint error format to special-case. A 400 from /process/localize and a 404 from a job lookup return the same shape – only the status code and the message text differ.
Match on the status code, not the message text
The HTTP status code is the stable signal – branch your error handling on it. The message string is written for a human reading a log; treat it as a description, not a machine-readable error code, and don't pattern-match on its exact wording.
Status codes#
Seven status codes cover every response. They are grouped here by who resolves them – because that grouping is also your retry policy.
You sent something the request can't satisfy (fix the request, don't blind-retry):
| Status | Meaning |
|---|---|
400 Bad Request | Request validation failed – a missing field, an invalid locale, an HTTP (not HTTPS) callbackUrl, a malformed payload. |
401 Unauthorized | The X-API-Key header is missing or invalid. See Authentication. |
403 Forbidden | The key is valid but has no access to the requested resource. |
404 Not Found | The resource – an engine, a job, a job group – does not exist. |
Your organization has hit an account limit (resolve in billing):
| Status | Meaning |
|---|---|
402 Payment Required | The organization has hit its credit limit. |
429 Too Many Requests | The organization has hit its daily token quota. Upgrade the plan to raise the limit. |
Something failed on our side (transient – retry):
| Status | Meaning |
|---|---|
500 Internal Server Error | An unexpected failure – a database error, or the translation call failed across every configured model in the engine. |
A 401 and a 403 look similar but are not the same problem: 401 means we could not identify the caller at all, 403 means we identified the key and it is not allowed in. The fix for 401 is the key itself (rotate or check it); the fix for 403 is the key's access.
Which errors to retry#
A skeptical integrator's first question about any error table is the one it usually leaves unanswered: which of these do I retry? The grouping above is the answer.
- 4xx – do not blind-retry. A
400,401,403, or404describes a condition in your request. Retrying the identical request reproduces the identical error. Fix the input, the key, or the resource id, then send it again. - 402 and 429 – back off, then resolve the limit. These are not transient at the request level; the next request hits the same wall until the underlying limit moves. Stop retrying in a tight loop, surface the limit, and resolve it (top up, or upgrade the plan).
- 500 – retry with backoff. This is the one class that is genuinely transient. A
500can mean every configured model timed out on that call; a retry may land on a healthy model. Use exponential backoff and a retry ceiling.
The async API reports outcomes differently
This retry policy is for synchronous calls you make yourself. The async localization API does not hand you a status code for the outcome of the work: a POST returns 202 once the request is accepted, and each target locale runs as an independent job via durable background workflows. You poll the job or receive a webhook for the result instead of catching a status code on your original call. See where async job errors live.
402 vs 429: two different limits#
The two account-level codes read alike – both sound like "you ran out" – and conflating them sends a developer to the wrong fix. They are distinct limits with distinct resolutions:
402 Payment Required– the organization has hit its credit limit. This is a billing boundary. The next call keeps failing until your organization's billing state changes.429 Too Many Requests– the organization has hit its daily token quota. This is a usage ceiling that resets, and you raise it by upgrading the plan.
The reason to keep them separate in your handler: a 402 is a billing action a person takes; a 429 is a quota you either wait out or lift by upgrading. Routing both to a generic "payment problem" message hides which lever the operator actually needs to pull.
A 402 body looks like every other error – the status code is what tells you it is a credit limit:
{
"message": "Organization has reached its credit limit"
}Where async job errors live#
There is a line worth drawing, because it is where a status-code handler stops being the right tool.
The status codes on this page are transport-level: they describe whether the API accepted and could serve your HTTP request. A 202 from the async API means your request was accepted – not that the translation succeeded. An asynchronous job can be accepted cleanly and still fail later, when a model times out mid-run. That failure is not an HTTP status code on your original call; it is captured on the job itself.
So async failures surface in three places, none of them this table:
- Per-job status. A failed locale carries
status: "failed"and anerrorMessageon the job. See job statuses. - Group status. When some locales succeed and others fail, the group reports
partial– the succeeded locales still ship. See tracking a job group. - Webhook delivery. A failure is delivered as a
translation.failedevent with anerrorfield. See webhook delivery.
There is one more distinction that catches people: a non-critical pipeline stage failing does not fail the job. The job completes with completed_with_warnings and per-step warnings instead of an error. That is a pipeline observability concern, not an error code – see observe pipeline runs.
Next steps#
A clean error handler starts with the two codes you will see first while integrating – authentication, and the points where async work reports its own outcome.
