Errors
Status codes, error format, and when to retry.
The public API uses standard HTTP status codes. Every error body is a
JSON object with a detail key, but detail itself takes one of three
shapes. Branch on the HTTP status first, then read detail.
Coded - 400, 402, 409, 429, and most 403s. code is stable
and safe to branch on; message is prose and may be reworded.
{
"detail": {
"code": "mode_not_in_plan",
"message": "Mode 'max' is not available on the 'starter' plan. Upgrade to access it."
}
}Plain - 401 and 503. These predate the coded shape.
{
"detail": "Invalid API key"
}Validation - a 422 caused by a malformed body. Here detail is a
list, described under Validation below.
{
"detail": [{ "type": "missing", "loc": ["body", "prompt"], "msg": "Field required" }]
}detail is not always a string. String(err.detail).toLowerCase() breaks
the first time it sees a 403 or a 429.
Every error response also carries an X-Request-Id header. Quote it
when reporting issues.
Status codes
| Code | Meaning | Retry? |
|---|---|---|
200 | Success | - |
400 | Malformed body or invalid field. Don't retry without fixing. | No |
401 | Missing or invalid X-API-Key. | No |
402 | No active subscription, or quota exhausted. | No (fix billing first) |
403 | Selected mode isn't available on your plan (code: mode_not_in_plan). | No (pick a lower mode or upgrade) |
404 | Endpoint or resource not found. | No |
409 | An earlier call with the same Idempotency-Key is still in flight. | Yes, once it completes |
422 | Validation error on the request body. | No |
429 | Rate limit exceeded. Honour Retry-After. | Yes, after Retry-After |
500 | Bug on our side. Please report with X-Request-Id. | Maybe (idempotent calls) |
503 | Agent capacity saturated. | Yes, with backoff |
Error codes
Most errors carry a machine-readable code inside detail.
code | Status | What happened | What to do |
|---|---|---|---|
invalid_idempotency_key | 400 | Idempotency-Key isn't 1-255 printable ASCII characters. | Fix the header value. |
invalid_query_string | 400 | A query string was sent alongside Idempotency-Key. | Move the parameters into the body. |
streaming_unsupported_for_structured_output | 400 | output_schema was combined with stream=true. | Set stream=false; structured output is non-streaming. |
quota_exceeded | 402 / 429 | The plan's request quota is spent. | Wait for X-Quota-Reset, or upgrade. |
mode_not_in_plan | 403 | The requested mode isn't on your plan. | Use a lower mode, or upgrade. |
no_active_subscription | 403 | The key is valid but the account has no active plan. | Subscribe, then retry. |
mcp_origin_not_allowed | 403 | A browser Origin hit the MCP endpoint from an untrusted host. | Call MCP server-side. |
idempotent_request_in_flight | 409 | An earlier call with the same Idempotency-Key is still running. | Retry once it finishes. |
idempotency_key_mismatch | 422 | The same Idempotency-Key was reused with a different body. | Use a fresh key for a different request. |
invalid_output_schema | 422 | output_schema isn't a usable JSON Schema. | Fix the schema. |
rate_limited | 429 | The per-key rate limit was hit. | Back off for Retry-After seconds. |
401 and 503 carry no code - branch on the status for those.
Validation (422)
Anatomy of a 422
Each entry returns the field path and an explanation. Treat the
loc array as the JSON pointer into your request body.
{
"detail": [
{
"type": "missing",
"loc": ["body", "prompt"],
"msg": "Field required"
}
]
}When to retry
Retry 429 and 503 with exponential backoff. Cap retries (we
recommend three) and surface the failure to the caller; the agent is
non-idempotent in the sense that two calls produce two answers and two
charges, so don't retry blindly on 500.
Send an Idempotency-Key to remove that
caveat: with one, a retry replays the original response instead of
running - and billing - a second time.
Report an issue
Send us:
- The
X-Request-Idfrom the failing response. - The HTTP status and body.
- The approximate timestamp (UTC).
- The mode and prompt shape (no need to share the full prompt content if it's sensitive - the request id is enough on our side).