> ## 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

> One error body for the whole API: a closed set of type values, a stable code, and the request id inside the body.

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.

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

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.

| `type`                  | Meaning                                                   |
| ----------------------- | --------------------------------------------------------- |
| `invalid_request_error` | The request as sent cannot be served. Fix the request.    |
| `authentication_error`  | The key is missing, malformed, or one we will not accept. |
| `permission_error`      | Authenticated, but not allowed to do this.                |
| `not_found_error`       | The thing addressed does not exist.                       |
| `rate_limit_error`      | A limit refused this request. Read `code` to learn which. |
| `insufficient_quota`    | The account cannot pay for this request.                  |
| `api_error`             | A fault on our side, or a state we cannot serve from.     |
| `server_error`          | Ours rather than yours, and expected to clear. Retry.     |

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

| Status | `type`                  | `code`                         | Meaning                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------ | ----------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 400    | `invalid_request_error` | `null`                         | A required parameter is missing. `param` names it.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 400    | `invalid_request_error` | `model_id_not_pinned`          | The `model` field is none of the forms this API accepts. It does not begin with a provider we serve, it is not a `profile/` slug, and it is not `hopscotch/auto`. `param` is `model`. This is a 400 and not a `model_not_found` 404 on purpose: nothing was looked up, because the string does not say which provider should serve it, and what is missing is a prefix rather than a correct spelling.                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 400    | `invalid_request_error` | `context_length_exceeded`      | The request is too large to price or serve. `param` is `messages`. Send less in one call.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| 400    | `invalid_request_error` | `unsupported_value`            | A field carries a value this API cannot honour. Today that is `store: true` on `POST /v1/responses`, because this platform stores no response: `param` is `store`, and the fix is to send `false` or leave the field out. Refused before admission, so nothing is held and no provider is called.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 400    | `invalid_request_error` | `provider_key_refused`         | The request ran on your own key for that provider, and the provider rejected the key itself or refused this particular call under it. Replace the key on the Provider keys screen; retrying will not change the answer.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| 401    | `authentication_error`  | `invalid_api_key`              | No key, a non-Bearer scheme, a key of the wrong shape, or a key we will not accept. See [Authentication](/api-reference/authentication).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 401    | `authentication_error`  | `key_expired`                  | The key is past its own expiry date. The message names the date it expired on. Checked before the per-key rate limiter, so an expired key never consumes that allowance. Expired keys are not renewed: create a new one.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 402    | `insufficient_quota`    | `insufficient_credit`          | The workspace cannot cover the estimated maximum cost of this request. The message names both figures. Buy credit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 402    | `insufficient_quota`    | `key_limit_exceeded`           | The sending key reached its own credit limit. Refused before any credit is held; other keys in the workspace are unaffected. The message names what the key has settled and holds inside its current window.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 402    | `insufficient_quota`    | `member_limit_exceeded`        | The person this request is attributed to has reached their own monthly spend limit. Refused before any credit is held and before any provider is attempted. It stops that one person, on every key their spend answers for and in the playground, while their colleagues go on serving from the same balance. The window is the calendar month, and only the workspace owner can raise or clear the figure.                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 402    | `insufficient_quota`    | `workspace_limit_exceeded`     | The workspace has reached its own monthly spend limit, which is the widest of these four ceilings. Refused before any credit is held and before any provider is attempted, and the usage record carries a charge of 0. It stops every member, on every key that carries the projected ceiling and in the playground unconditionally, so it is the code you get when a request is past both this ceiling and a member's. There is no self-service control for the figure yet: contact us to change it.                                                                                                                                                                                                                                                                                                                                                        |
| 403    | `permission_error`      | `model_not_permitted`          | The model, or every servable member of the routing profile, is outside this workspace's permitted set. The set is unrestricted by default, so this reaches only a workspace somebody restricted. Nothing to retry: a workspace owner has to widen the set.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| 404    | `invalid_request_error` | `endpoint_not_supported`       | The path is not one this release serves, whether or not it exists upstream. The message names the method and the path and says it is not supported in this release.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| 404    | `not_found_error`       | `model_not_found`              | No model of that id is in the live catalog, which is what a typo produces. Check the spelling, or list what this key can call with `GET /v1/models`. `param` is `model` on the model routes and `null` on an inference call.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 404    | `not_found_error`       | `model_not_available`          | The id is catalogued and is not being served right now, either at all or on the endpoint you called. Told apart from `model_not_found` so a withdrawn model does not read as a typo. The status and the code are the same in every case; the **message** says which case it is, and whether waiting helps. A model the provider has retired says it is gone rather than unwell, that retrying will not bring it back, and that no change to the request reaches it. A model the provider still serves that we cannot reach right now says that instead, and says that nothing about your request is wrong. Anything else keeps the sentence this code has always carried: known here, not being served at the moment, and retrying later is worth more than changing the request. No message names a replacement model unless the provider named one itself. |
| 404    | `invalid_request_error` | `model_not_available`          | The same code from the other direction, on a request that sent `hopscotch/auto`: `param` is `model`, and the message names your cost tier, because that is the fact that decides what you do next. Either the tier has no published list, or nothing in it can be served right now. Both ways out are named in the message: pick another cost tier, or create a route and make it the default.                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| 405    | `invalid_request_error` | `method_not_allowed`           | A method other than GET on a path that exists. Carries an `Allow` header. Answered before any key is looked at, so it costs nothing and reveals nothing.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| 413    | `invalid_request_error` | `payload_too_large`            | The request body is larger than the 8388608 byte maximum this API accepts, and it was refused at the door. A body that declares its length is refused without a byte being read; one that declares none is read only as far as the ceiling and then abandoned. Nothing was parsed, no credit was held, no model was called and nothing was charged. The message names the figure and what to change: fewer messages, a shorter prompt, or fewer inputs.                                                                                                                                                                                                                                                                                                                                                                                                      |
| 429    | `rate_limit_error`      | `rate_limit_exceeded`          | This key's request rate limit. Carries `Retry-After`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 429    | `rate_limit_error`      | `spend_rate_exceeded`          | The account's spend rate cap. Nothing to fix; requests are admitted again as the window rolls forward.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 429    | `rate_limit_error`      | `upstream_rate_limited`        | The model's provider rate-limited the call. Nothing about the request is wrong and nothing is charged for it. Carries `Retry-After` in whole seconds: the provider's own figure where it sent one, and five seconds where it sent none. Back off and retry.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 429    | `rate_limit_error`      | `provider_key_refused`         | The request ran on your own key for that provider, and the provider says that key is over its limit there. Nothing to fix on our side; it clears on its own.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 500    | `api_error`             | `null`                         | An unhandled fault on our side. Quote the request id.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| 503    | `api_error`             | `no_provider_configured`       | We are not currently configured to serve this. Retry later.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| 503    | `server_error`          | `paused_by_owner`              | Somebody with access to this account has paused it. Retrying on a timer will not clear it; the pause has to be lifted.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 503    | `server_error`          | `balance_unavailable`          | We could not read the account's balance, so the request was refused rather than served unmetered. Ours. Retry shortly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| 503    | `server_error`          | `model_not_priceable`          | No credit could be held for the call. Usually ours, and clears on its own: retry shortly. One case is not, and the message says so, because it is the request rather than the model that is missing something: a request naming no output ceiling, for a model that publishes no maximum output either. Send `max_tokens` and it is admitted. See [Request parameters](/api-reference/parameters).                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| 503    | `server_error`          | `model_pricing_unreadable`     | As above, from the other direction: the published rate for the model could not be read. Ours. Retry shortly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| 503    | `server_error`          | `model_permissions_unreadable` | The document saying which models this workspace may call could not be read, so the request was refused rather than admitted as if the set were unrestricted. Ours. Retry shortly.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| 504    | `server_error`          | `request_deadline_exceeded`    | The request ran past the 540 second limit and was terminated. Split the work, or stream the response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |

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](/concepts/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.

```
data: {"error":{"message":"Your credit ran out while this request was still running, so it was stopped rather than finished. You have been charged only for what was produced before that point. Add credit and send it again.","type":"insufficient_quota","param":null,"code":"balance_exhausted","request_id":"00000000-0000-4000-8000-000000000000"}}
```

Three codes can end a stream this way:

| `code`                      | `type`               | Meaning                                                                                                                                      |
| --------------------------- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `balance_exhausted`         | `insufficient_quota` | Credit ran out while the request was running. You are charged for what was produced before the cut.                                          |
| `balance_unavailable`       | `server_error`       | We lost sight of the balance while the request was running and stopped it rather than leave it unwatched. Not a statement about your credit. |
| `request_deadline_exceeded` | `server_error`       | The 540 second limit landed mid-stream.                                                                                                      |

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

<CodeGroup>
  ```python Python theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
  from openai import APIStatusError

  try:
      completion = client.chat.completions.create(
          model="openai/gpt-4o-mini",
          messages=[{"role": "user", "content": "Say hi in five words."}],
      )
  except APIStatusError as error:
      body = error.body or {}
      detail = body.get("error", {})
      code = detail.get("code")
      request_id = detail.get("request_id") or error.request_id

      if code == "insufficient_credit":
          pass  # Buy credit, then retry.
      elif code == "rate_limit_exceeded":
          pass  # Read Retry-After from error.response.headers and back off.
      elif code == "paused_by_owner":
          pass  # Do not retry on a timer. The pause has to be lifted.
      else:
          raise
  ```

  ```typescript TypeScript theme={"theme":{"light":"vitesse-light","dark":"vesper"}}
  import OpenAI from 'openai';

  try {
    const completion = await client.chat.completions.create({
      model: 'openai/gpt-4o-mini',
      messages: [{ role: 'user', content: 'Say hi in five words.' }],
    });
  } catch (error) {
    if (!(error instanceof OpenAI.APIError)) throw error;

    const detail = (error.error ?? {}) as { code?: string; request_id?: string };
    const requestId = detail.request_id ?? error.requestID;

    switch (detail.code) {
      case 'insufficient_credit':
        break; // Buy credit, then retry.
      case 'rate_limit_exceeded':
        break; // Read Retry-After from error.headers and back off.
      case 'paused_by_owner':
        break; // Do not retry on a timer. The pause has to be lifted.
      default:
        throw error;
    }
  }
  ```
</CodeGroup>

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