Errors
Every error type, its HTTP status and what to do.
Errors use HTTP status codes and a JSON body with a stable, machine-readable type:
{"error": {"message": "human readable", "type": "insufficient_credits", "code": 402,
"retryable": false, "charged": false}}Branch on type, not on message: messages are for humans and may change. Every error also carries two booleans:
retryable: sending the same request again, unchanged, can succeed. Wait forRetry-Afterwhen it is set, otherwise back off briefly. When it isfalse, change something first (the request, the key, the model, your balance) or wait forresets_at.charged: this request was billed. It isfalsefor every error returned before any output. It can betrueonly on an error event inside a stream, after output reached you: you pay for that output, and the final usage chunk has the exact cost.
$0, and so do our internal retries. The only partial charge is a stream that stops after output started (an upstream failure mid-stream, or your balance or a key limit running out): you pay for the prompt and the output already sent. See No charge on failure.Error types
| HTTP | type | When | What to do |
|---|---|---|---|
| 400 | invalid_request | Malformed body, missing model or messages, or a parameter the model rejected. | retryable: false. Fix the request; do not retry unchanged. |
| 400 | max_price_exceeded | The model's price is above your max_price (prompt or completion, USD per 1M tokens). Refused before anything is reserved; error.pricing has the model's prices. | retryable: false. Raise max_price or choose a cheaper model. |
| 400 | zdr_unavailable | You sent zdr: true and the model has no zero-data-retention route. | retryable: false. Pick a model whose data_retention.zero_retention_available is true in GET /models. |
| 401 | invalid_api_key | Missing, unknown, disabled, revoked or expired key. | retryable: false. Check the Authorization header and the key's status. |
| 402 | insufficient_credits | Available balance is below the request's hold (for a stream, its first slice). Mid-stream, it arrives as an error event when the balance or key limit can't cover more output; you pay only for what was sent. | retryable: false. Add credits, or lower max_tokens to reduce the hold. |
| 402 | key_limit_exceeded | The request's maximum possible cost (prompt plus max_tokens) is more than one of the key's spending limits has left. error.limit says which: total, daily or monthly; daily and monthly add error.resets_at. Mid-stream, it arrives as an error event. | retryable: false. Set a lower max_tokens, raise the key's limit, or wait for resets_at. |
| 402 | purchase_required | The model is available after your first credit purchase (its supplier limits daily use), and this account has not bought credit yet: the sign-up credit alone does not unlock it. Refused before anything is reserved; the message names a model you can use now. GET /models shows requires_purchase: true for these models. | retryable: false. Buy credits once (any amount), or use the suggested model. The gate lifts within 30 seconds of the purchase. |
| 403 | account_suspended | The account is suspended. | retryable: false. Contact support. |
| 404 | model_not_found | Unknown or not publicly available model. | retryable: false. Check the slug against GET /models. |
| 404 | not_found | Unknown path (only the endpoints in this reference exist), or a generation id that is not yours. | retryable: false. Check the URL and base URL. |
| 405 | method_not_allowed | Wrong HTTP method for the path, e.g. GET on /chat/completions. The Allow header lists the right one. | retryable: false. Use the method in the Allow header. |
| 408 | timeout | The model did not respond in time and nothing was charged. | retryable: true. Retry with backoff. For long non-streamed outputs, use stream: true. |
| 413 | invalid_request | Request body larger than 32 MiB. | retryable: false. Send less data (e.g. smaller or fewer images). |
| 429 | rate_limited | Per-key request rate reached (600 per minute by default). Retry-After header is set. | retryable: true. Wait Retry-After seconds, then retry. |
| 429 | concurrency_limited | The key already has as many requests running as its limit allows. Retry-After: 1. | retryable: true. Retry when one of your requests finishes, or raise the key's limit. |
| 429 | key_token_limit_exceeded | The key used its daily token limit. Retry-After and error.resets_at give the reset (00:00 UTC). | retryable: false. Wait for the reset, or raise the key's limit. |
| 429 | model_capacity | The model is at capacity right now and nothing was charged. Retry-After says when it should be back. | retryable: true. Wait Retry-After seconds, or use another model. |
| 500 | internal_error | Something failed on our side. Nothing was charged. | retryable: true. Retry with backoff; contact support if it persists. |
| 503 | upstream_error | Every eligible upstream failed. Nothing was charged. | retryable: true. Retry with backoff. |
| 503 | no_available_endpoint | All endpoints for the model are disabled or paused. Retry-After is set when one is due back. | retryable: true. Retry later or use another model. |
Long non-streamed requests
If a non-streamed request is still generating after 80 seconds, we start the response with status 200 and send whitespace every few seconds to keep the connection open. JSON parsers ignore it, and the OpenAI SDKs handle it without changes. Because the status is already sent, an error that happens after that point arrives as the usual {"error": {...}} body, with the real status in error.code. Check for an error key before reading choices, or use stream: true for long outputs.
Retries
Retry when error.retryable is true, with exponential backoff and jitter, and honour Retry-After. Clients that only see the status can retry 408, 429, 500 and 503, except the 429 key_token_limit_exceeded, which lasts until the daily reset. Within a single request we already fail over between upstream endpoints for the same model before returning 503.
import openai
try:
reply = client.chat.completions.create(model=model, messages=messages)
except openai.APIStatusError as e:
# The SDK stores the error object itself in e.body (and its type in e.type)
error = e.body if isinstance(e.body, dict) else {}
if getattr(e, "type", None) == "insufficient_credits":
notify_billing_owner() # top up, or retry with a lower max_tokens
elif error.get("retryable"):
schedule_retry(e) # backoff; honour Retry-After
else:
raise