Skip to main content
This guide turns the OpenType error contract into client code: how to read an error, which failures a retry can fix, and a retry helper for POST /v1/runs that never charges you twice. Read it before you put an integration in front of real traffic. For the list of every code, see Errors.

The error envelope

Every JSON error has one shape:
What the response does not carry:
  • No retryable field. The status and the code tell you whether to retry.
  • No Retry-After header. You choose the backoff.
  • No WWW-Authenticate header on a 401.

Match on code, never on message

Messages can mislead as well as change: every provider_* code shares the message “the provider call failed”, and the deadline_exceeded message says “before a route was chosen” even when the model call is what timed out. When you meet a code your client does not know, fall back to its status family below.

Errors that are not JSON

A few rejections come from the web layer before OpenType’s own handlers run. Their bodies are plain text or empty, and they have no request_id in the body. The x-request-id response header is still set. The runs routes (/v1/runs...) wrap the same failures into JSON: 400 invalid_body and 413 body_too_large. Everywhere else, parse defensively: try JSON, and if that fails, keep the status, the raw text and the x-request-id header.

What to do, by status

A 429 is always a quota refusal, never a rate limit, so retrying it quickly does not help. Nothing was stored, so a later retry may reuse the same key. The helper below leaves 429, like every other 4xx, to your code.

Why a 5xx needs a new idempotency key

POST /v1/runs stores the run before it calls the model. When a 5xx comes after that point, the stored run does not run again under the same key:
  • If the run failed after a model call, it is settled as failed. The same key replays it as 200 with "state": "failed".
  • If the run failed before a model call (no_route_available, decision_unavailable, deadline_exceeded, most provider_* codes), it stays pending. The same key replays it as 202 indefinitely.
So a retry after a 5xx must use a new key. A retry after no response must use the same key, because you do not know whether the first attempt was stored. Idempotency has the full table.

A retry helper for POST /v1/runs

The helper:
  • sends the body with a key derived from your business id,
  • retries 500, 503 and 504 with exponential backoff and jitter, and a new key on each attempt (<base>:r1, <base>:r2, …),
  • retries a timeout or a dropped connection with the same key,
  • never retries a 4xx,
  • returns the run, including a 200 with "state": "failed" or a 202 with "state": "pending", for you to handle.
Keep the base key short enough to take the suffix: the whole header must be 255 bytes or fewer.
Use it with a key derived from your own id:
Make the suffixed keys deterministic, as above. If your process restarts halfway through the retries, it sends the same sequence of keys again and picks up the stored runs instead of creating new ones.

Retrying other routes

Only POST /v1/runs takes an idempotency key. For the rest:

What goes wrong

  • Errors - every code, grouped by status, with its message.
  • Problem codes - one page per code, with causes and fixes.
  • Idempotency - when a key replays, conflicts, or must be replaced.
  • Request ids - send your own x-request-id and find a request later.
  • API reference - request and response schemas for every route.