uttapen

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"
    }
  }
}
  • code and type always hold the same value; branch on code.
  • message is written for humans and is fine to surface to an end user. Messages that originate at the provider are passed through as they arrive.
  • uttapen is present only when we have something useful to add, so check for it before reading it.
  • The X-Uttapen-Request-Id header is set on errors too. Send it along when you report a problem.

The error table

StatusCodeWhenWhat to do
400invalid_requestMalformed 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 valueFix the body; retrying changes nothing
400invalid_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 capacityDrop the parameter or lower max_tokens
401invalid_api_keyNo key, wrong key, revoked or expiredCreate a new key in the dashboard; don't retry
402insufficient_balanceAvailable balance is below the hold this request needs, or the balance is negativeTop up (uttapen.topup_url) or lower max_tokens; the request never reached the provider
403account_suspendedAn admin suspended the accountContact support
403model_not_allowedThe model is outside this key's allowed_modelsUse another model, or widen the key
404model_not_foundThe model id doesn't exist, is hidden or was removed; or the id has no provider prefixTake the id from /v1/models
404not_foundThe route doesn't exist, or it's /v1/responses, which isn't enabled yetUse /v1/chat/completions
413request_too_largeBody larger than 20 MBShrink the image, or send extracted PDF text
429rate_limit_exceededMore requests in a minute than the key's rate_limit_rpmWait for Retry-After
429key_monthly_limit_reachedThe key's monthly limit is used up (uttapen.monthly_limit_toman, spent_toman)Raise the limit or use another key
429too_many_concurrent_streamsMore than 10 open streams for this accountLet one finish; Retry-After: 5
429free_quota_exceededYour daily free-model quota is spent (uttapen.alternative_model)Switch to a paid model, or come back tomorrow
429free_capacity_exhaustedThe provider's shared free capacity for that model is exhaustedUse a paid model; respect Retry-After
429upstream_rate_limitedThe provider is throttling this model right nowWait a few seconds and retry, or use a comparable model
502upstream_errorA 5xx from the provider, a truncated response, or a mid-stream failureRetry with backoff; the hold is released, or settled against what was actually consumed
503upstream_unavailableWe couldn't reach the provider, or no capacity is available at allRetry-After: 30; retry
503pricing_unavailablePricing isn't configured yet (only during initial setup)Try again in a few minutes
504upstream_timeout120 seconds without a single byte from the providerRetry; for slow models use stream: true
other 4xxthe provider's own codeA provider error passed through with its statusRead 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:

StatusException
400openai.BadRequestError
401openai.AuthenticationError
402openai.APIStatusError (no dedicated class; check status_code == 402)
403openai.PermissionDeniedError
404openai.NotFoundError
413openai.APIStatusError
429openai.RateLimitError
502, 503, 504openai.InternalServerError
Dropped connection / timeoutopenai.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 of message — wording can change.
  • For 429 and 5xx, use exponential backoff with jitter and at most three attempts. Respect Retry-After when it's there.
  • Surface 402 to your user or your own admin and stop retrying; nothing changes until the wallet is topped up.
  • Log X-Uttapen-Request-Id next 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/me once to see the key, its allowed models and the balance in one place.