Errors come back as JSON with an error object, in the shape the OpenAI SDKs expect. The HTTP status tells you what class of problem it is; the code tells you which one.
The envelope
{"error": {"message": "You are out of credits.","type": "billing_error","code": "insufficient_credits","param": null,"request_id": "req_9a2f..."}}
Only message and type are guaranteed
Most endpoints fill incode, param and request_id, but not all of them do. Branch on the HTTP status first and on code second, with a fallback — do not assume code is always present.Status codes#
| Status | Means | Retry? |
|---|---|---|
| 400 | Your request is malformed — a bad parameter, an unsupported field, or media the model cannot take. | No. Fix the request. |
| 401 | Missing or invalid credentials. | No. |
| 402 | Out of credits, no active plan, or a daily spend cap was hit. | No. Add credits. |
| 403 | Authenticated, but not allowed — scope, plan, or a blocked account. | No. |
| 404 | Unknown model or resource. Also returned by speech-to-text when that feature is turned off. | No. |
| 409 | Video content requested before it finished. | Yes, after polling. |
| 413 | Upload over 100 MB. | No. |
| 429 | Rate limited. | Yes — honour Retry-After. |
| 500 | Something broke on our side. The message is always generic. | Yes, with backoff. |
| 503 | Every upstream for that model was unavailable. | Yes, shortly. |
Error types#
| type | When |
|---|---|
invalid_request_error | Validation failed (400). |
auth_error | Credentials or permissions (401, 403). |
billing_error | Credits or plan (402). |
rate_limit_error | Too many requests (429). |
not_found | Unknown model, file, task or route (404). |
server_error | Ours (500, 503). |
The codes you will actually meet#
Bad requests
unknown_model— the id is not one of the three. OpenAI model names land here.media_not_supported— you attached an image to Bala, or video to anything but Rishi.model_generation_unsupported— you asked a model to generate media it cannot.context_window_exceeded— your messages are longer than the model’s window.conflicting_parameters— you sent bothmax_tokensandmax_completion_tokens. Send one.invalid_tool_arguments— a tool call in your message history hasargumentsthat are not valid JSON.unsupported_parameter— a field we deliberately reject; the message names the replacement.
Billing
A 402 carries extra fields so you can render a useful prompt rather than a dead end.
402 with an action
{"error": {"message": "You are out of credits.","type": "billing_error","code": "insufficient_credits","balance": 0,"action": "add_credits","action_label": "Add credits","action_url": "https://upfyn.com/dashboard/ai/billing"}}
insufficient_credits— balance is exhausted.no_active_plan— the account has no plan.daily_spend_limit_exceeded— a cap you set was reached; it clears at the next window.
Rate limits
user_rate_limit_exceeded— the account’s per-minute request limit.key_rate_limit_exceeded— that one key’s limit.temporary_auto_block— repeated 429s triggered a short cool-off. Back off properly rather than retrying harder.
Rate limits has the numbers and the headers.
Errors during a stream#
Once a stream has started, the response is already committed to 200 — the status cannot change. A failure therefore arrives as a frame in the stream, followed by the usual terminator.
A stream that failed halfway
data: {"id":"chatcmpl-...","choices":[{"delta":{"content":"The answer is"}}]}data: {"error":{"message":"Upstream failed.","type":"server_error","code":"upstream_stream_error"}}data: [DONE]
Check every frame for an error key
A streaming client that only readschoices[0].delta will treat a failed turn as a short one and show the user a truncated answer with no indication anything went wrong.Retrying well#
Retry 429, 500 and 503. Never retry 400, 401, 402 or 403 — they will fail identically and a retry loop on a 402 is just noise. Honour Retry-After when it is present.
Backoff that respects Retry-After
import time, randomfrom openai import OpenAI, APIStatusErrorclient = OpenAI(base_url="https://ai.upfyn.com/v1", api_key=KEY)RETRYABLE = {429, 500, 502, 503, 504}def complete(**kwargs):for attempt in range(5):try:return client.chat.completions.create(**kwargs)except APIStatusError as err:if err.status_code not in RETRYABLE:raise # 400/401/402/403 will never succeed on retrywait = float(err.response.headers.get("Retry-After") or 0)if not wait:wait = (2 ** attempt) + random.random() # backoff with jittertime.sleep(wait)raise RuntimeError("gave up after 5 attempts")
Idempotency
Send anIdempotency-Key header on a request you might retry, and a repeat of the same key will not be billed twice.