Error codes — 400 to 504 and what to do
Every uttapen API error from 400 to 504 — the JSON shape, what each code means, whether it costs you anything, and how the OpenAI SDKs raise it.
Updated: September 7, 2026
Every error comes back in OpenAI's error shape, so the SDKs turn it into the exception you expect. The one thing we add is an extra uttapen block, which carries something actionable on the errors where that helps — the amount you're short and a top-up link, for instance.
Error shape
{
"error": {
"message": "Insufficient balance. This request needs about 151,656 toman and your available balance is 97,518 toman.",
"type": "insufficient_balance",
"code": "insufficient_balance",
"param": null,
"uttapen": {
"balance_toman": "97517.828964",
"available_toman": "97517.828964",
"required_toman": "151656.439570",
"topup_url": "https://uttapen.ir/dashboard/wallet"
}
}
}
codeandtypealways hold the same value; branch oncode.messageis written for humans and is fine to surface to an end user. Messages that originate at the provider are passed through as they arrive.uttapenis present only when we have something useful to add, so check for it before reading it.- The
X-Uttapen-Request-Idheader is set on errors too. Send it along when you report a problem.
The error table
| Status | Code | When | What to do |
|---|---|---|---|
400 | invalid_request | Malformed JSON, missing model, a numeric field sent as a string or decimal, embeddings asked of a model that has none, a malformed plugins or models value | Fix the body; retrying changes nothing |
400 | invalid_request (from upstream) | A parameter the model doesn't accept (temperature on a reasoning model, an image sent to a text-only model), or a request larger than the provider's momentary capacity | Drop the parameter or lower max_tokens |
401 | invalid_api_key | No key, wrong key, revoked or expired | Create a new key in the dashboard; don't retry |
402 | insufficient_balance | Available balance is below the hold this request needs, or the balance is negative | Top up (uttapen.topup_url) or lower max_tokens; the request never reached the provider |
403 | account_suspended | An admin suspended the account | Contact support |
403 | model_not_allowed | The model is outside this key's allowed_models | Use another model, or widen the key |
404 | model_not_found | The model id doesn't exist, is hidden or was removed; or the id has no provider prefix | Take the id from /v1/models |
404 | not_found | The route doesn't exist, or it's /v1/responses, which isn't enabled yet | Use /v1/chat/completions |
413 | request_too_large | Body larger than 20 MB | Shrink the image, or send extracted PDF text |
429 | rate_limit_exceeded | More requests in a minute than the key's rate_limit_rpm | Wait for Retry-After |
429 | key_monthly_limit_reached | The key's monthly limit is used up (uttapen.monthly_limit_toman, spent_toman) | Raise the limit or use another key |
429 | too_many_concurrent_streams | More than 10 open streams for this account | Let one finish; Retry-After: 5 |
429 | free_quota_exceeded | Your daily free-model quota is spent (uttapen.alternative_model) | Switch to a paid model, or come back tomorrow |
429 | free_capacity_exhausted | The provider's shared free capacity for that model is exhausted | Use a paid model; respect Retry-After |
429 | upstream_rate_limited | The provider is throttling this model right now | Wait a few seconds and retry, or use a comparable model |
502 | upstream_error | A 5xx from the provider, a truncated response, or a mid-stream failure | Retry with backoff; the hold is released, or settled against what was actually consumed |
503 | upstream_unavailable | We couldn't reach the provider, or no capacity is available at all | Retry-After: 30; retry |
503 | pricing_unavailable | Pricing isn't configured yet (only during initial setup) | Try again in a few minutes |
504 | upstream_timeout | 120 seconds without a single byte from the provider | Retry; for slow models use stream: true |
other 4xx | the provider's own code | A provider error passed through with its status | Read the message |
5xx and 429 are safe to retry: none of them cost you anything unless you actually received a response. Don't retry 400, 401, 403 or 404 — the outcome will be identical.
What errors cost
The rule is simple: no response, no charge. Errors raised before the request goes out never place a hold at all, and errors from the provider release the hold immediately. The single exception is a failure part-way through a stream, where some of the response did arrive: there, only what was actually consumed up to the cut is settled (streaming).
How the SDKs raise it
Python (openai) — one class per status:
| Status | Exception |
|---|---|
400 | openai.BadRequestError |
401 | openai.AuthenticationError |
402 | openai.APIStatusError (no dedicated class; check status_code == 402) |
403 | openai.PermissionDeniedError |
404 | openai.NotFoundError |
413 | openai.APIStatusError |
429 | openai.RateLimitError |
502, 503, 504 | openai.InternalServerError |
| Dropped connection / timeout | openai.APIConnectionError, openai.APITimeoutError |
import openai
try:
resp = client.chat.completions.create(model="openai/gpt-5-mini", messages=msgs)
except openai.AuthenticationError:
raise SystemExit("The uttapen API key is invalid")
except openai.RateLimitError as e:
wait = int(e.response.headers.get("retry-after", "5"))
time.sleep(wait)
except openai.APIStatusError as e:
if e.status_code == 402:
info = e.body["error"]["uttapen"]
print("Insufficient balance; needed:", info["required_toman"], "toman —", info["topup_url"])
else:
print(e.status_code, e.code, e.message)
e.code is our error.code, e.body is the whole JSON body, and e.response.headers gives you the headers, x-uttapen-request-id included. By default the SDK retries 429 and 5xx twice with backoff; tune that with max_retries.
Node.js (openai) — the same names: AuthenticationError, RateLimitError, NotFoundError, BadRequestError, PermissionDeniedError, InternalServerError. For 402 you get the generic APIError with err.status === 402. Both err.code and err.error.uttapen are available.
try {
await client.chat.completions.create({ model: "openai/gpt-5-mini", messages });
} catch (err) {
if (err instanceof OpenAI.APIError) {
console.error(err.status, err.code, err.error?.uttapen);
} else throw err;
}
PHP (openai-php/client) — every HTTP error arrives as OpenAI\Exceptions\ErrorException with getStatusCode(), getErrorCode() and getMessage(). This client drops the uttapen block, so for 402 either read the amount out of the message or fetch the raw body with Guzzle.
Go (openai-go) — var apierr *openai.Error; errors.As(err, &apierr), then apierr.StatusCode, apierr.Code, apierr.Message, and apierr.RawJSON() for the uttapen block.
Advice for production code
- Branch on
code, never on the text ofmessage— wording can change. - For
429and5xx, use exponential backoff with jitter and at most three attempts. RespectRetry-Afterwhen it's there. - Surface
402to your user or your own admin and stop retrying; nothing changes until the wallet is topped up. - Log
X-Uttapen-Request-Idnext to the error on your side. Give us that id and we'll find the request in seconds. - Before you deploy, call
GET /v1/uttapen/meonce to see the key, its allowed models and the balance in one place.