Skip to content

Error reference

Every code the gateway returns, what causes it, and whether retrying can help.

5 min read

The envelope

Every failure, on every endpoint, comes back in OpenAI’s shape. Clients built against OpenAI already parse it — inventing our own would mean every SDK’s error handling silently stopped working the moment someone pointed it at us, which is the easiest possible way to lose a migration.

402 Payment Required
{
  "error": {
    "message": "Insufficient credit: balance $0.004120, this request could cost up to $0.061400. Top up at /dashboard/credits.",
    "type": "billing_error",
    "code": "insufficient_credit",
    "request_id": "req_…"
  }
}

type is one of invalid_request_error, authentication_error, permission_error, not_found_error, rate_limit_error, billing_error, idempotency_error or api_error — the coarse class, for a switch statement. code is the precise reason, and it is the one to branch on. request_id also travels on the X-Multigrid-Request-Id header, on failures as well as successes.

Messages are written to be acted on
An error that can be fixed says where. Running out of credit quotes your balance and the estimate to six decimal places and points at the top-up page; a wrong-scope key explains which of the two kinds you are holding. An unexpected internal failure is the one exception — it returns a deliberately generic message, because an unhandled throw can carry a stack, a URL with a key in it, or a chunk of your request body.

Every code

StatusCodeRetryMeaning
400invalid_requestNoMalformed body, an unusable value in one of our own fields, or a 4xx passed back from a provider. The message names what is wrong.
401unauthenticatedNoNo Authorization header, or it is not a bearer token.
401invalid_api_keyNoThe token is not a Multigrid key, or no key matches it.
401key_revokedNoThe key existed and has been disabled.
402insufficient_creditNoThe balance cannot cover this request’s worst case. Nothing was sent upstream.
402key_limit_exceededNoThis key’s own lifetime spend cap.
402monthly_limit_exceededNoThe account’s monthly ceiling, which you set. Not a payment failure.
402no_free_routeNoA :free model id found no zero-cost route. Add your own provider key, or drop the suffix.
402byok_key_rejectedNoThe provider rejected your own key, so the request was not sent. Deliberately not failed over: moving it onto a paid route would quietly start billing you for traffic you had arranged to pay the provider for directly, and you would find out on an invoice. Re-enter the key, or remove it to pay with credit.
403wrong_key_scopeNoAn inference key on a management endpoint, or a provisioning key on an inference one. type is permission_error: no amount of retrying turns one kind of key into the other, and it used to go out as api_error, which every SDK retry wrapper reads as “our fault, back off and try again” — forever.
403account_suspendedNoThe account is suspended. Every key on it fails identically.
404model_not_foundNoNo model in the catalogue has that id. GET /models is the list.
404not_foundNoA key, request or batch id that does not exist on your account. Another customer’s id reads the same way, which is all it should tell you.
409request_in_flightYesAnother request with the same Idempotency-Key is still upstream. Retry with backoff to collect its result.
413payload_too_largeNoAn attachment, or the request as a whole, is over the size cap. The message states both figures in MB.
422guardrail_blockedNoOne of your own guardrails refused it. The message names the rule and says whether it was charged.
429rate_limitedYesYour key’s per-minute limit, or an upstream one we could not route around. Retry-After is authoritative.
500internal_errorYesAn unexpected failure on our side. The request was not billed.
502upstream_errorYesEvery provider we could try failed. The attempts are on the request row.
503no_provider_availableNoThe model exists but nothing on this deployment can serve it — no adapter for the provider, or no key for it.
503plugin_unavailableNoA plugin you asked for cannot run here. Refused rather than skipped: an answer without the web search you requested is the model’s recollection dressed as research, and you could not tell.
504upstream_timeoutYesNo provider responded inside the deadline — 60 seconds non-streaming, 300 streaming.

OAuth is the one endpoint that does not use this envelope. The token endpoint answers in the RFC 6749 shape so that any OAuth client library parses it; those codes are listed with the OAuth flow.

Which errors cost money

Almost none. Everything refused before the balance pre-flight — 401, 403, 404, 409, 413, 429, and an input-stage guardrail block — happens before any provider is contacted and cannot appear on an invoice. So does a 402: that is the pre-flight.

  • An output-stage guardrail_blocked is charged. The response was really generated and we were really billed for it. The message says so.
  • A stream that fails mid-answer is charged for the tokens produced before the break, including when your own client hangs up.
  • A web search that ran is charged even if the model then failed. The backend was called and billed us before the model started.

Anything charged has a matching ledger entry and a cost you can look up by request id.

Errors during a stream

Once a stream has started there is no status code left to set — the 200 is already on the wire. The failure arrives as an SSE event carrying the same envelope:

text/event-stream
event: error
data: {"error":{"message":"Groq: upstream connection reset","type":"api_error","code":"upstream_error","request_id":"req_…"}}

Every token produced before the break is still delivered, and the terminal usage chunk still follows.

The official OpenAI SDKs will not surface this
They iterate data: lines and ignore named SSE events, so event: error reaches them and is discarded. What your code sees is a stream that ends early: a truncated answer, no exception, no error. It is worth saying plainly because the failure looks like the model simply stopping, and a summariser that returns half a summary without complaining is the sort of bug that reaches production.

If a mid-stream failure has to be detectable in your code, read the raw lines rather than using the SDK’s iterator, or check finish_reason on the terminal chunk — a stream that broke has none. GET /generation with the request id is the authoritative answer after the fact.

Something here disagrees with what the API actually did? That is a bug in this page, and worth reporting.

Report it