> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hopscotchlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors and refusals

> One error body for the whole API, a closed set of types, and a request id on every response, so a failure tells you what to do next.

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.

```json Error envelope theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
{
  "error": {
    "message": "Incorrect API key provided.",
    "type": "authentication_error",
    "param": null,
    "code": "invalid_api_key",
    "request_id": "REQUEST_ID"
  }
}
```

| Field        | Type           | Notes                                                                       |
| ------------ | -------------- | --------------------------------------------------------------------------- |
| `message`    | string         | Prose for a person. It may be reworded, so do not branch on it.             |
| `type`       | string         | The category an SDK branches on. A closed set.                              |
| `param`      | string or null | The request field at fault, or `null`. Present and null rather than absent. |
| `code`       | string or null | The stable machine-readable reason. Branch on this.                         |
| `request_id` | string         | Our id for this request. Quote it when you contact support.                 |

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.

```python theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
detail = (error.body or {}).get("error", {})

if detail.get("code") == "insufficient_credit":
    ...
```

The full table of statuses, types, and codes is in the
[API error reference](/api-reference/errors).

## 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](/api-reference/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 one of three codes, and the code is the part
to read: `rate_limit_exceeded` for your key's request rate, `spend_rate_exceeded`
for the account's spend rate cap, or `upstream_rate_limited` for the model's
provider rate-limiting the call. Back off for the first. For the second, sending
`max_tokens` lowers what each request reserves; see
[Rate limits and spend controls](/concepts/rate-limits-and-spend-controls). The
third is not your allowance and not your cap: nothing about the request is wrong
and nothing is charged for it. `rate_limit_exceeded` and `upstream_rate_limited`
both carry `Retry-After` in whole seconds.

**The body was too large.** `413` with `payload_too_large`. The maximum request
body this API accepts is 8388608 bytes, eight mebibytes, and a larger one is
refused at the door: a body that declares its length is refused without a byte
being read, and one that declares none is read only as far as the ceiling and
then abandoned. Nothing is parsed, no credit is held, no model is called and
nothing is charged. Send fewer messages, a shorter prompt, or fewer inputs.

**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.

<Note>
  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.
</Note>

## 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 `upstream_rate_limited`: retry after `Retry-After` seconds. The
  figure is one 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.
* `413` with `payload_too_large`: do not retry unchanged. Send a smaller body.
* `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.
