One body
Every error on the API, at every status, is one JSON object with a single top-levelerror 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 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.429withrate_limit_exceeded: retry afterRetry-Afterseconds.429withspend_rate_exceeded: retry, and consider sendingmax_tokensso each request reserves less.429withprovider_key_refused: retry with backoff. It carries noRetry-After, but a provider’s own limit clears on its own.403withmodel_not_permitted: do not retry. The permitted set has to be widened by a workspace owner.400withmodel_id_not_pinned: do not retry unchanged. Add the provider prefix, sogpt-4o-minibecomesopenai/gpt-4o-mini, or send aprofile/slug.404withmodel_not_found: do not retry unchanged. Fix the model id.404withmodel_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.503withpaused_by_owner: do not retry on a timer.- Other
503, and500: 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.