Errors

Non-2xx responses use this envelope. Branch on error.code, not on the English message.

{
  "error": {
    "code": "not_found",
    "message": "Project not found",
    "request_id": "…"
  }
}

Always log request_id and send it to support. The same value is also on the X-Request-ID response header.

Do not parse a top-level detail string — that shape is gone on /v1 (one exception below).

Validation

400 / 422 with validation_failed may include field_errors:

{
  "error": {
    "code": "validation_failed",
    "message": "…",
    "request_id": "…",
    "field_errors": [
      {
        "field": "source_language_code",
        "message": "Field required",
        "type": "missing"
      }
    ]
  }
}

field is the public field name (source_language_code, workflow.0, …). It is null when the offending name has no public equivalent.

field_errors is not always there, and when it is missing the key is absent — not null. You get it when the request failed against the request schema. You do not get it when the rejection is a rule about the request as a whole:

  • an unknown language code (unknown_locale_codes: …)
  • an unknown source language (unknown source_language: …)
  • the source language also listed as a target (source_in_targets: …)
  • an invalid workflow (invalid_workflow: …)
  • target_language_codes that is not a JSON array of strings
  • an unsupported query parameter on GET /v1/projects

All of those return validation_failed with the detail in error.message and no field_errors key at all. Read error.message in that case, and always reach for it defensively — error.get("field_errors", []), never error["field_errors"].

Common codes

Code Status Meaning
invalid_api_key 401 Missing, unknown, or revoked key
insufficient_scope 403 Valid key, missing scope (required_scope is set)
forbidden 403 Authenticated, but not allowed for this resource
not_found 404 Resource missing or not visible to this key — including an unknown project
method_not_allowed 405 Wrong verb on a real route
gone 410 Route removed on purpose; the body names the replacement when there is one
validation_failed 400 / 422 Bad body or query — see the note on field_errors above
missing_api_version 400 You sent X-API-Version with a value other than 1 (omitting the header is fine)
payment_required 402 Credits short — envelope also has required, available, shortfall
payload_too_large 413 Upload over 200 MB total or 100 files
idempotency_in_progress 409 Idempotency-Key already used — in flight, or its outcome is unknown
idempotency_conflict 409 Generic 409 fallback. Create never emits it
rate_limited 429 Honor Retry-After and X-RateLimit-*
upstream_unavailable 502 / 503 / 504 Safe to retry idempotent calls
internal_error 500 Unexpected failure — include request_id

Exception: POST /v1/projects/{id}/resumptions

That one credit-gate 402 is still flat, with no error object:

{
  "detail": "Insufficient credits",
  "required": 100,
  "available": 30,
  "shortfall": 70
}

A 409 on the same route is also flat (detail, current_status). Its sibling POST /v1/projects/{id}/review/resumptions uses the normal envelope — the two resumption routes do not agree on error shape, so branch on the route, not on a shared handler.

What to do

  1. Read error.code.
  2. For validation, walk field_errors.
  3. For 402, top up credits and call the matching resume route.
  4. For 429, wait for Retry-After / X-RateLimit-Reset.
  5. Keep request_id when you open a ticket.