Errors and retries
Every error an adapter hits, whether to retry it, and the in-product fallback.
Every /v1 error uses the same envelope. Read body.detail.code to branch in code, show
body.detail.message to your operator logs, and treat body.detail.fix as the one-line
remediation hint (it is written for a coding agent):
The HTTP status is authoritative for retry policy; code is authoritative for which
thing went wrong. The two rules that hold across the whole surface:
404, never403, for resources — an id you do not own looks identical to one that does not exist (timing-safe, so an attacker cannot probe other tenants). A403is only ever about your key or a demo-key restriction, not about a missing resource.402fires before the expensive work — over-budget message sends and image generations are rejected pre-flight, so a402means nothing was charged and no model ran.
A handful of envelope edge cases are not the {code, message, fix} shape — the two
auth failures (401, and the invalid/revoked 403) and the two infrastructure 503s
return a plain-string detail. They are called out explicitly in the tables below so
your client does not assume detail.code always exists.
Retry decision in one line
429,502,500 internal_error, and the two infrastructure503s are retryable — back off and try again (the500/502/503cases are transient;429after its window).- Everything in the
4xxrange below (except429) is a caller fix, not a retry — retrying the same request reproduces the same error. Correct the request, the character setup, the budget, or the key first. 409 session_exitedis terminal for that session — start a new session; do not retry.
Request and authoring errors (4xx — fix the request)
These come from a malformed or out-of-range request body. None are retryable as-is; fix the
named field and resend. validation_error is the one you will hit most: it is what every
Pydantic field error (missing slug, missing display_name, a voice_samples entry with no
text, a forbidden_style entry that is a string not an object, an out-of-range
behavioral_thresholds value, …) is rendered as, with the failing loc: msg quoted into
message.
State conflicts (409 — fix the state, then proceed)
A 409 means the request is well-formed but the current state does not allow it.
Retrying the identical call will keep failing until you change the underlying state (upload
identity, publish a version, start a new session, …).
Budget (402 — before any spend)
Auth (401 / 403 — fix the key)
These are about your key, never a missing resource. Two of them (401, and the
invalid/revoked 403) return a plain-string detail, so do not read detail.code for
those — branch on the HTTP status.
Demo sandbox key issuance (POST /v1/demo/sandbox-key)
These are specific to minting a no-auth demo key. The exact caps are in Authentication.
For a visual POC or a repeated eval loop, do not fight the demo issuance rate limit — mint a
persistent staging key (POST /v1/apps → POST /v1/api-keys, which requires an existing
key for your tenant) and reuse it. See Staging and evaluation.
Server / transient (5xx — retry with backoff)
Failures that arrive as events, not HTTP errors
A reply is asynchronous, so a reply that fails after POST /messages was accepted does
not surface as an HTTP error — POST /messages already returned 202-style accepted
with a cursor. Watch the event log for:
message.failed(role: "system") — the reply for a turn failed. Payload is{ turn_index, reason, recoverable }. Ifrecoverableis true, the turn is eligible for the platform’s reply-recovery sweep; if false, prompt the user to resend.
So a robust client handles errors in two places: the synchronous error envelope on the
call you made, and message.failed on the session event stream.
Client checklist
- Branch on
body.detail.codefor the envelope errors; branch on HTTP status for the plain-string auth401/403and the infrastructure503s. - Retry only
429(after its window),502,500 internal_error, and the infrastructure503s — with backoff. Never blind-retry a4xxrequest error. - Treat
409 session_exitedas terminal: open a new session. - Treat
402 budget_exceededas “out of credits,” not “try again now.” - Subscribe to
message.failedso an async reply failure is not silently lost.