|
Documentation
Book a DemoPlatform
PlatformMCPCLIAPI
Workflows
GuidesChangelog

Welcome

  • Overview
  • Authentication
  • Errors & status codes
  • Webhook signatures

Localization

  • Overview
  • Create jobs
  • Lock non-translatable keys
  • Track a job group
  • Get a single job
  • List jobs
  • Webhook delivery
  • Live progress (WebSocket)

Pipeline

  • Overview
  • Pre-localization AI edit
  • Human review
  • AI review (post-edit)
  • Rephrase for natural copy
  • Back-translation check
  • Configure the pipeline
  • Observe pipeline runs

Provisioning

  • Overview
  • Create a provisioning job
  • Source types
  • What the AI extracts
  • Webhook delivery
  • Live progress (WebSocket)

Synchronous

  • Localize
  • Recognize

Errors & status codes

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.

json
{
  "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):

StatusMeaning
400 Bad RequestRequest validation failed – a missing field, an invalid locale, an HTTP (not HTTPS) callbackUrl, a malformed payload.
401 UnauthorizedThe X-API-Key header is missing or invalid. See Authentication.
403 ForbiddenThe key is valid but has no access to the requested resource.
404 Not FoundThe resource – an engine, a job, a job group – does not exist.

Your organization has hit an account limit (resolve in billing):

StatusMeaning
402 Payment RequiredThe organization has hit its credit limit.
429 Too Many RequestsThe organization has hit its daily token quota. Upgrade the plan to raise the limit.

Something failed on our side (transient – retry):

StatusMeaning
500 Internal Server ErrorAn 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, or 404 describes 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 500 can 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:

json
{
  "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 an errorMessage on 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.failed event with an error field. 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.

Authentication
Fix 401 and 403 – how the X-API-Key header and org scoping work
API Keys
Rotate or re-issue a key when you hit a 401
Track a job group
Where a partial async failure surfaces – succeeded locales still ship

Was this page helpful?

Max PrilutskiyMax Prilutskiy·Updated about 3 hours ago·6 min read