Skip to main content
Every response on this API with a status of 400 or above carries the same body: one top-level error object, in the shape an OpenAI-compatible client already parses. Your existing error handling keeps working, and the fields below are the contract.

The 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. Three properties of this body are worth stating because clients depend on them:
  • param and code are always present. When there is nothing to say they are null rather than omitted, so a missing field never has to be told apart from a null one.
  • request_id is stamped on every error body, whatever produced the error. It is the same value as the x-hopscotch-request-id and x-request-id response headers.
  • No other field appears inside error. Nothing about our internals travels in an error body, and a body that would carry anything else is replaced before it leaves us.
Branch on type and code. Messages are prose written for a person, and we reword them; the fields are what we hold still.

type

A closed set. A value outside it is not something this API can send, so a client may treat the set as exhaustive. api_error and server_error both mean “not your request”. The split matters for retries: api_error with no code is an unhandled fault that will most likely fail the same way again, while server_error codes name a condition that clears.

Status and code

Every code below is produced by a shipped handler. code is the field that separates causes sharing a status. A request that never reached a model is not charged for, and the credit held for it is returned. A request terminated part way through is charged for what was produced before it was cut and no more.

Rate limits and spend refusals

Three codes share 429, and they call for different behaviour. Read the code: the status alone does not tell you which of the three you met.
  • rate_limit_exceeded is your key sending too many requests. The response carries Retry-After in seconds. Back off and retry.
  • spend_rate_exceeded is the account’s spend rate cap, which counts the estimated maximum cost reserved in a rolling window rather than settled spend. There is no Retry-After. Sending max_tokens on your requests lowers the estimated maximum each one reserves. See Rate limits and spend controls.
  • upstream_rate_limited is the model’s provider rate-limiting the call. It is not your key’s allowance and not your account’s cap, nothing about the request is wrong, and nothing is charged. It carries Retry-After in whole seconds.
Retry-After is present on rate_limit_exceeded and on upstream_rate_limited, and is not sent on other statuses. Do not infer a retry delay from its absence. The Retry-After on upstream_rate_limited is a figure we compute rather than the provider’s header passed through: their number where they sent one, held between one and sixty seconds, and five seconds where they sent none. A wait of zero is never sent, and no wait longer than a minute is, whatever the provider asked for. A provider that answers 529 rather than 429 carries the same upstream_rate_limited code, with type api_error as the status class requires. The code is the part to branch on. The message on upstream_rate_limited is our generic one, the same sentence every fault on our side carries, and that is deliberate rather than an oversight: a throttle on a credential of ours is not a fact about your request, and the provider’s own words about it name our account. The code and the Retry-After are the actionable half, which is why the code is what to read.

Provider key refusals

provider_key_refused only appears on a request that ran on your own key for that provider, never on a Hopscotch-held key for it. While your workspace holds that key, every request to the provider runs on it alone, and nothing falls back to a different credential if the provider refuses it. The code is the same at both statuses; what differs is why the provider refused the request:
  • 429 is the provider saying that key is over its limit there. It clears on its own, so back off and retry.
  • 400 is the provider rejecting the key itself, or refusing this particular call under it. Retrying the same request will be refused identically until the key is replaced on the Provider keys screen.
Neither status carries Retry-After, and neither is charged.

Errors during a streamed response

Once a streamed response has started, its status and headers are already sent, so a failure cannot change them. Instead the stream ends with one more data: line carrying the same error body, and then closes. There is no data: [DONE] after it.
Three codes can end a stream this way: balance_exhausted is only ever a mid-stream code. A request refused before it started because the account cannot pay is 402 with insufficient_credit, so a client can always tell “never started” from “cut off part way”. The request_deadline_exceeded line is the one error body on this API without a request_id: it is written after the response has left the point that stamps the id. Read the id from the x-hopscotch-request-id response header instead. A non-streamed response that we terminate part way through does not receive an error body. There is nowhere to put one, so the connection errors instead. Treat a truncated non-streamed body as a failed request rather than a short answer.

Handling errors in code

Catch on the fields, never on the message.
Every response carries the same id on two headers: x-hopscotch-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.