Errors
The error envelope, the status codes we return, and what each one means.
Every failure this API returns uses one JSON shape, whatever went wrong and wherever it went wrong.
The envelope
{
"error": {
"code": 404,
"message": "No route for GET /api/v1/nope",
"metadata": { "error_type": "not_found" }
}
}
Three rules hold across the whole surface:
error.coderepeats the HTTP status. Not an application-specific code, not a string — the same integer you already have on the response line. If you are reading a body you have already read a status, and they will agree.messageis for a human. Log it, show it to a developer, do not branch on it. Its wording is not a contract and it is the part most likely to be reworded.metadata.error_typeis for a program. It is the stable identifier, and it is what aswitchshould read. It is present whenever the failure can be classified, andmetadatamay carry extra members alongside it — a rejected parameter's name, a requested model id — depending on the failure.
Status codes
Each status has a default error_type, which a more specific failure may
override. A 400 caused by an over-long prompt, for instance, reports
context_length_exceeded rather than the generic invalid_request.
| Status | Default error_type | What it means here |
|---|---|---|
| 400 | invalid_request | The request was malformed, missing a required field, or exceeded a limit on the prompt. |
| 401 | authentication | No credential, or one we do not recognise. |
| 402 | payment_required | The account has no spendable credit, or the key's own spend cap is used up. |
| 403 | permission_denied | The credential is valid and is not permitted to do this. |
| 404 | not_found | No such route, model, or generation. |
| 408 | timeout | The request did not complete in time. |
| 409 | — | A conflict on a management resource. This status carries no canonical type. |
| 410 | — | Permanently removed. Returned by exactly one endpoint, a retired purchase route. |
| 412 | precondition_failed | A precondition on the request was not met. |
| 413 | payload_too_large | The request body exceeded the size limit. |
| 422 | unprocessable | Well-formed, but not something we can act on. |
| 429 | rate_limit_exceeded | Too many requests. Carries Retry-After and X-RateLimit-* when it comes from us rather than from a provider. |
| 500 | server | Something failed on our side. See the note on masking below. |
| 502 | provider_unavailable | The chosen provider was down, or returned nothing usable. |
| 503 | provider_overloaded | No endpoint could satisfy the request, or the provider was saturated. |
| 504 | timeout | The provider timed out. |
| 524 | timeout | The request timed out at the edge before we answered. |
| 529 | provider_overloaded | The provider is temporarily overloaded. |
Of these, the four below were run against the live deployment while this page was written. The rest are the statuses the gateway is built to return; treat the table as the set your client should handle, and the samples as the ones we can show you verbatim.
Captured responses
400 — a required field is missing. A completion request with no model:
{"error":{"code":400,"message":"`model` is required.","metadata":{"error_type":"invalid_request"}}}
401 — no credential.
{"error":{"code":401,"message":"Missing Authentication header","metadata":{"error_type":"authentication"}}}
403 — valid credential, wrong class. An inference key reading the balance:
{"error":{"code":403,"message":"Only management keys can perform this operation","metadata":{"error_type":"permission_denied"}}}
404 — a model the catalogue does not hold. Note the extra members alongside
error_type: they name what was asked for and why the lookup ended.
{"error":{"code":404,"message":"No endpoints found for nonexistent/model.","metadata":{"requested":"nonexistent/model","resolved":"nonexistent/model","reason":"model_not_in_catalogue","error_type":"not_found"}}}
An unknown path returns the same status through a different route, naming the method and URL:
{"error":{"code":404,"message":"No route for GET /api/v1/nope","metadata":{"error_type":"not_found"}}}
500 responses are masked
A 500 always reads Internal Server Error and carries no detail about what
actually failed or which provider was involved. The real cause is written to our
logs against the request id, and the request-id response header is how you
point us at it. Do not parse a 500 message for meaning — there is none in it by
design.
Failures during a stream
A streaming request that has already begun sending data cannot be given a 4xx or
5xx status: the status line went out with the first byte and said 200. So a
failure after that point arrives in the stream instead, as one final chunk
carrying an error member and finish_reason: "error", after which the stream
ends.
The consequence for a client is the part worth internalising: an HTTP 200 on a
streaming request does not mean the generation succeeded. A client that only
checks the status and concatenates deltas will silently treat a failed generation
as a short one. Check the last chunk for an error member. On this shape
metadata.error_type is always present — an unclassifiable failure reports
unmapped rather than omitting the field.
We are not printing a sample of that chunk here, because inducing a mid-stream failure on a live deployment is not something these docs can do read-only, and every sample on this site is one that was actually run.
Rate limits
Two budgets apply to /api/v1, and both are counted in a fixed window:
| Budget | Limit | What it is for | | --- | --- | --- | | Per API key | 120 requests / minute | The ceiling that governs a single caller. | | Whole endpoint | 6000 requests / minute | A shared backstop against a flood. Normal use never reaches it. |
A refusal from either budget is a 429 with error_type rate_limit_exceeded,
and it carries four headers describing the budget that actually refused:
x-ratelimit-limit: 120
x-ratelimit-remaining: 0
x-ratelimit-reset: 1786765134
retry-after: 59
x-ratelimit-reset is a unix timestamp in seconds; retry-after is a
count of seconds to wait. Prefer retry-after — it is unambiguous. These
headers appear on 429 responses only, not on successful ones.
Two details worth knowing before you tune a client around this:
- The limit is charged before your key is checked. A request with a bad or
expired key still spends budget. A retry loop hammering a rotated key will
earn a
429on top of its401s. - Reading the catalogue counts.
GET /modelsand friends are unauthenticated, but a request that carries anAuthorizationheader is still charged to that key. Poll the catalogue without one if you are polling it often.
Retrying
429, 502, 503, 504, 524 and 529 are the transient set — retry with
backoff. A 429 raised by the gateway carries Retry-After; honour it rather
than picking your own interval.
400, 401, 403, 404, 413 and 422 will not improve on retry. Fix the
request or the credential.
402 means the account is out of credit; see Credits and
usage.