Skip to content

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 body
{"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 for Retry-After when it is set, otherwise back off briefly. When it is false, change something first (the request, the key, the model, your balance) or wait for resets_at.
  • charged: this request was billed. It is false for every error returned before any output. It can be true only 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.
Errors are free. A request that ends in any of these errors before producing output costs $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

HTTPtypeWhenWhat to do
400invalid_requestMalformed body, missing model or messages, or a parameter the model rejected.retryable: false. Fix the request; do not retry unchanged.
400max_price_exceededThe 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.
400zdr_unavailableYou 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.
401invalid_api_keyMissing, unknown, disabled, revoked or expired key.retryable: false. Check the Authorization header and the key's status.
402insufficient_creditsAvailable 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.
402key_limit_exceededThe 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.
402purchase_requiredThe 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.
403account_suspendedThe account is suspended.retryable: false. Contact support.
404model_not_foundUnknown or not publicly available model.retryable: false. Check the slug against GET /models.
404not_foundUnknown path (only the endpoints in this reference exist), or a generation id that is not yours.retryable: false. Check the URL and base URL.
405method_not_allowedWrong 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.
408timeoutThe model did not respond in time and nothing was charged.retryable: true. Retry with backoff. For long non-streamed outputs, use stream: true.
413invalid_requestRequest body larger than 32 MiB.retryable: false. Send less data (e.g. smaller or fewer images).
429rate_limitedPer-key request rate reached (600 per minute by default). Retry-After header is set.retryable: true. Wait Retry-After seconds, then retry.
429concurrency_limitedThe 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.
429key_token_limit_exceededThe 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.
429model_capacityThe 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.
500internal_errorSomething failed on our side. Nothing was charged.retryable: true. Retry with backoff; contact support if it persists.
503upstream_errorEvery eligible upstream failed. Nothing was charged.retryable: true. Retry with backoff.
503no_available_endpointAll 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