Errors & retries
Every endpoint except chat answers with an envelope carrying error.kind; chat speaks OpenAI-shaped errors. Below is what each kind means and what to do about it.
The envelope
Success and failure share one shape: success, code, data, error. An error always carries kind and message, and sometimes retry_after_sec — a recommended pause in seconds.
{
"success": false,
"code": 429,
"data": null,
"error": {
"kind": "rate_limited",
"message": "rate limit exceeded (rpm)",
"retry_after_sec": 12
}
}{
"error": {
"message": "no healthy text provider",
"type": "invalid_request_error"
}
}Error kinds
| kind | HTTP | What happened | Retry? |
|---|---|---|---|
| invalid_request | 400 | The body failed validation: no prompt or messages, malformed aspect_ratio, unknown provider. | No — fix the request |
| unauthorized | 401 | The key is missing, revoked or invalid. | No — check the key |
| forbidden | 403 | Access refused, e.g. an expired signature on a file URL. | No |
| not_found | 404 | The object does not exist — a foreign or purged job, for instance. | No |
| rate_limited | 429 | You hit the plan rpm or concurrency ceiling. | Yes — after a pause |
| quota_exceeded | 429 | The daily or lifetime quota of the plan is exhausted. | No — until reset or upgrade |
| no_healthy_provider | 503 | No eligible provider is currently available. | Yes — with backoff |
| provider_error | 502 | Every provider the gateway tried returned an error. | Yes — with backoff |
| timeout | 504 | The upstream did not answer in time. | Yes — with backoff |
| internal_error | 500 | An internal gateway failure. | Yes — with backoff |
What the gateway already does
One /v1 call is one attempt on your side, but not necessarily one attempt inside. The gateway walks its provider pool, honouring health and circuit-breaker state; a finished job reports how many it needed in attempts. Failed internal attempts cost you nothing.
What to do on your side
- Honour retry_after_sec when it is present — it is the gateway’s own calculation, not a guess.
- For a 429 caused by rpm, use exponential backoff with jitter; for quota_exceeded, retrying is pointless.
- Never retry 400, 401, 403 or 404 — the same body yields the same answer.
- For image generation, retry the job poll rather than the POST, or you create a duplicate job and pay the quota twice.
Handling it in code
import time, httpx
def call_with_retry(request, attempts=4):
delay = 1.0
for attempt in range(attempts):
response = request()
if response.status_code < 400:
return response
body = response.json()
kind = (body.get("error") or {}).get("kind")
# Client mistakes and auth failures never get better on retry.
if response.status_code in (400, 401, 403, 404):
response.raise_for_status()
# The gateway tells you how long to wait when it knows.
wait = (body.get("error") or {}).get("retry_after_sec") or delay
if kind == "quota_exceeded":
raise RuntimeError("quota exhausted — upgrade the plan or wait for the reset")
time.sleep(wait)
delay *= 2
response.raise_for_status()