/api/v2/ is the same shape.
request_id is what to quote in a support ticket. It is also on the
X-Request-Id response header, including on responses that carry no body.
A failure that ends a run which had already started carries three more fields
beside the error object — id, status and trace_id — so you can tie the
failure to the run it ended:
The codes
blocked is not failed
Two of those seventeen are decisions rather than faults, and both end a run as
blocked:
Nothing went wrong upstream. There was no model failure. The platform declined to
spend, or asked you to slow down.
Filing either as
failed is what makes an error-rate dashboard useless and a
support answer wrong — “your integration is broken” when the truthful answer is
“you are out of credits”.
402 is deliberate. The two obvious alternatives are both misleading: 403
says the caller lacks permission, which is a different fix, and 429 says slow
down, which will never help. 402 is what a billing-gated API returns.
Retry after a delay
Retry after a delay
rate_limited (429) — honour any Retry-After.provider_unavailable (502), knowledge_unavailable (503) — transient.
Back off.Do not retry; a human has to act
Do not retry; a human has to act
usage_blocked (402) — retrying will never help. Top up or raise the cap.permission_denied (403) — the key is missing a scope. The message names it.invalid_request (400), structured_output_failed (422) — the request or the
schema has to change.Retry only if it is safe
Retry only if it is safe
internal_error (500), tool_execution_failed (502).For anything that writes to your systems, read
the unknown-outcome rule
first. A timeout on a write is not a failure — it is an unknown.Run statuses
A run ends in one of four terminal statuses:queued and running are in flight, and a caller polling one may see it change.
cancelled carries no error object. Attaching a fault to a stop would put it
back into every error-rate panel that counts one, which is exactly what giving it
its own status was meant to avoid.
Nothing internal leaks
message is the only text that reaches you, and it defaults to a fixed per-code
string. The diagnostic detail — the exception, the id that was guessed, the
provider that timed out — is logged against request_id and is not serialised.
There is no field that carries it outward.
So a message never contains a stack trace, a provider name, a database id, or
any prompt text.
The one place this reads oddly is preview: a caller without agent:preview
asking to run a draft gets 404, not 403. Confirming that a draft exists but may
not be run is exactly the confirmation the rule exists to withhold.
invalid_context is a 403 and not a 422, for a related reason. A request
context naming something you may not reach is an authorization refusal wearing a
payload’s clothes. It is not an existence oracle either: the check is set
membership in your own grants, so a resource that does not exist and one that
exists elsewhere answer identically.
Three codes you may not expect
Errors elsewhere in the platform
The/api/v1/ surface and the Reseller API predate this
envelope and do not all use it. Everything under /api/v2/ does.
