Skip to main content
Two different things can go wrong with a request, and this API keeps them apart. A refusal is us working correctly and saying no. Your credit will not cover the call, your key is sending faster than its limit, somebody paused the account. Nothing is broken, nothing is charged, and there is something specific to do about it. An error is a fault: ours, or a request we cannot serve as sent. There is still something to do about it, and the body says what. Both arrive in the same shape, so your client needs one code path for them and tells them apart on a field rather than on a status alone.

One body

Every error on the API, at every status, is one JSON object with a single top-level error key. An OpenAI client parses it with the error handling it already has.
Error envelope
Branch on type and code. Both are stable. message is prose and may change. The type values are invalid_request_error, authentication_error, permission_error, not_found_error, rate_limit_error, api_error, insufficient_quota, and server_error. The same body comes back from every failing route on the API. It is the shape an OpenAI-compatible client already parses, which is the point: your existing error handling keeps working after you change the base URL.

Read the fields, not the message

type is a closed set. code is stable. message is prose written for a person to read, and we reword it when a clearer sentence exists. So branch on type and code. A client matching on message text breaks the day somebody improves a sentence, and that is a bad way to find out.
The full table of statuses, types, and codes is in the API error reference.

The six things that go wrong

Grouped by what you would do about them rather than by status. Your request cannot be served as sent. A missing parameter, or a request too large to serve in one call. type is invalid_request_error and param names the field when we know it. Fix the request; retrying it unchanged will fail identically. Your key is not accepted. Missing, wrong shape, or not one we will serve. 401, and one sentence for every kind of key we will not accept, which is deliberate: see Authentication. Do not retry a 401; change the key. Your account cannot pay for it. 402 with insufficient_credit before the request starts, or the stream ending with balance_exhausted if credit runs out part way. Buy credit. Nothing is charged for the refusal, and a request cut off part way is charged for what it produced and no more. A limit refused it. 429, with rate_limit_exceeded for your key’s request rate (which carries Retry-After) or spend_rate_exceeded for the account’s spend rate cap. Back off for the first. For the second, sending max_tokens lowers what each request reserves; see Rate limits and spend controls. Your own provider key was refused. Only if you bring your own key for a provider: every request to it then runs on that key alone, and if the provider refuses it, nothing falls back to ours. code is provider_key_refused either way. A 429 means the provider says that key is over its limit there, which clears on its own. Anything else from the provider comes back as 400, because retrying the same request will not change the answer until the key is replaced on the Provider keys screen. Either way, nothing is charged. The model cannot be served. Three answers, told apart so that each one says what to do next. A 404 with model_not_found means no model of that id exists, which is what a typo produces: check the spelling, or list what your key can call with GET /v1/models. A 404 with model_not_available means we do know the id and are not serving it right now, either at all or on the endpoint you called. The status and the code stay the same whichever way that happened, so a client branching on either keeps working, and the message is what tells the three cases apart: a model the provider has retired says it is gone rather than unwell, and that neither retrying nor changing the request reaches it; a model the provider still serves that we cannot reach right now says that instead, and says plainly that nothing about your request is wrong; anything else says the model is known here, is not being served at the moment, and that retrying later is worth more than changing the request. None of the three names a replacement model unless the provider named one itself, because a guess at what you meant is worse in an error body than no guess at all. A 403 with model_not_permitted means the model is live and this workspace’s permitted set does not name it; that set is unrestricted by default, so this reaches only a workspace somebody restricted, and a workspace owner has to widen it. Nothing is charged for any of the three, and no provider was called. Ours. 500, 503, or 504. A 503 names its condition in code, and the codes differ in whether waiting helps: balance_unavailable, model_not_priceable, model_pricing_unreadable, and model_permissions_unreadable clear on their own, while paused_by_owner means a person paused the account and only a person can unpause it. Retrying that one on a timer will spend your backoff budget on a state no amount of waiting changes.

What an error never contains

Our error bodies carry the fields listed above and nothing else. No stack trace, no hostname, no internal identifier, no detail about the shape of our own system. That is not reticence for its own sake: none of it is anything you could act on, and all of it is a normal way for an error string to hand an attacker a map. What you get instead is the request id, and a support conversation that starts from it rather than from a description of what the screen said.
Every response carries the same id on two headers: x-uniblock-request-id, which is ours, and x-request-id, which is the one the official OpenAI SDKs surface on their error objects. Every error envelope repeats it as request_id. Quote it when you ask us about a request. Without it we are guessing at which of your requests you mean.

Retrying

  • 401, 402, 400: do not retry unchanged. Change the key, the credit, or the request.
  • 429 with rate_limit_exceeded: retry after Retry-After seconds.
  • 429 with spend_rate_exceeded: retry, and consider sending max_tokens so each request reserves less.
  • 429 with provider_key_refused: retry with backoff. It carries no Retry-After, but a provider’s own limit clears on its own.
  • 403 with model_not_permitted: do not retry. The permitted set has to be widened by a workspace owner.
  • 400 with model_id_not_pinned: do not retry unchanged. Add the provider prefix, so gpt-4o-mini becomes openai/gpt-4o-mini, or send a profile/ slug.
  • 404 with model_not_found: do not retry unchanged. Fix the model id.
  • 404 with model_not_available: read the message before you retry. If it says the provider has retired the model, do not retry at all; pick another model. If it says we cannot reach it, or says neither, retry later, because the request itself is fine.
  • 503 with paused_by_owner: do not retry on a timer.
  • Other 503, and 500: retry with backoff. If it persists, quote a request id.
  • 504: the request ran past the 540 second limit. Retrying the same work unchanged will most likely hit the same limit, so split it or stream it.