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_codesthat 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
- Read
error.code. - For validation, walk
field_errors. - For
402, top up credits and call the matching resume route. - For
429, wait forRetry-After/X-RateLimit-Reset. - Keep
request_idwhen you open a ticket.