# Errors and limits (https://hypit.ai/api-reference/errors/)

> One error envelope everywhere, what each status means, and the two rate limits that apply.

Every error on every route — `/v1`, `/v1beta`, and the unauthenticated endpoints — uses the same
body:

```json
{
  "error": {
    "message": "invalid api key",
    "type": "authentication_error",
    "code": "invalid_api_key",
    "param": null
  }
}
```

| Field     | Use it for                                                            |
| --------- | --------------------------------------------------------------------- |
| `code`    | **branch on this.** Stable, machine-readable, specific                |
| `type`    | the OpenAI-compatible category, for clients that already switch on it |
| `message` | show to a human. Redacted, but free-form and not a contract           |
| `param`   | always `null` today; present for OpenAI SDK compatibility             |

`type` is one of `invalid_request_error`, `authentication_error`, `permission_error`,
`not_found_error`, `rate_limit_error`, `insufficient_quota`, `upstream_error`, `timeout_error`,
`server_error`.

The `message` is scrubbed before it leaves us: provider names, base URLs, upstream model names and
credentials never appear in it. An upstream's raw text is kept in our logs only, which is why quoting
`X-Request-Id` in a support ticket is worth more than pasting the message.

## Status codes [#status-codes]

| Status | When                                                                           |
| ------ | ------------------------------------------------------------------------------ |
| `400`  | the request is malformed — a missing field, a bad enum, an unusable URL        |
| `401`  | no credential, an unknown or disabled key, or a disabled account               |
| `402`  | not enough credits                                                             |
| `403`  | an OAuth token lacks the scope for this route                                  |
| `404`  | unknown route, unknown model, or a job that is not yours                       |
| `409`  | an idempotency conflict, or a `/content` link asked for before an asset exists |
| `413`  | the body or an uploaded file exceeds its cap                                   |
| `425`  | an identical idempotent request is still being prepared — retry shortly        |
| `429`  | a rate limit, a concurrency limit, or too many open jobs                       |
| `499`  | the caller went away before we finished                                        |
| `500`  | our bug                                                                        |
| `502`  | the upstream failed or returned nothing usable                                 |
| `503`  | no provider is currently available, or our database is briefly unreadable      |
| `504`  | the upstream did not answer in time                                            |

Note `402` and `503`: neither is retryable in the same shape. A `402` needs credits; a `503` from
`no_available_provider` or `precheck_failed` should be retried with backoff, because it means our
side is briefly unable to serve, not that your request is wrong.

## Rate limits [#rate-limits]

Two separate limits, both per API key:

| Limit               | Default | On refusal                                             |
| ------------------- | ------- | ------------------------------------------------------ |
| requests per minute | **60**  | `429` `rate_limited`, `Retry-After: 60`                |
| concurrent requests | **15**  | `429` `too_many_concurrent_requests`, `Retry-After: 1` |

These are the defaults; a deployment may raise them. They are token-bucket and semaphore
respectively — requests are refused, never queued. An OAuth access token has no key row, so it is
limited per account instead.

A third limit applies only to asynchronous media: at most **64** `queued` or `running` jobs per
account, counted across every key and session it owns. Exceeding it is `429`
`media_open_job_capacity` with `Retry-After: 30`. See
[Asynchronous jobs](/api-reference/jobs#concurrency-limits).

There are no `X-RateLimit-*` headers. Read `Retry-After` and back off.

## Retrying safely [#retrying-safely]

* **`425`, `429`, `503`, `502`, `504`** — retry with exponential backoff and jitter. Honour `Retry-After`
  when present.
* **`4xx` other than `425` and `429`** — correct the request before retrying. `425 admission_in_progress` means the original submission is still being prepared; retry it with the same `Idempotency-Key`.
* **Creating a job** — send an `Idempotency-Key`. A retry with the same key returns the original job
  instead of creating and charging a second one.
* **Chat streaming** — a stream that fails after the first byte cannot be retried by us and stays at
  HTTP `200`. Check every SSE frame for an `error` key, and re-issue the whole request yourself if
  you need to.

## Codes you will see on every route [#codes-you-will-see-on-every-route]

| `code`                         | Status | Meaning                                                     |
| ------------------------------ | ------ | ----------------------------------------------------------- |
| `missing_token`                | 401    | no credential was found                                     |
| `invalid_api_key`              | 401    | the key does not exist                                      |
| `key_disabled` / `key_expired` | 401    | the key row is unusable                                     |
| `account_disabled`             | 401    | the owning account is suspended                             |
| `insufficient_scope`           | 403    | an OAuth token is missing this modality's scope             |
| `insufficient_credits`         | 402    | the balance cannot cover the call                           |
| `no_account`                   | 401    | the credential resolves to nothing billable                 |
| `precheck_failed`              | 503    | the credit ledger is briefly unreadable                     |
| `rate_limited`                 | 429    | requests per minute                                         |
| `too_many_concurrent_requests` | 429    | concurrent requests                                         |
| `model_not_found`              | 404    | the model is not routable for this operation under your key |
| `no_capable_provider`          | 404    | providers exist but none implements this capability         |
| `no_available_provider`        | 503    | every candidate is cooling down                             |
| `request_timeout`              | 504    |                                                             |
| `request_canceled`             | 499    | the client disconnected                                     |
| `not_found`                    | 404    | no route matches the method and path                        |
| `internal_error`               | 500    | our bug — quote the request id                              |

Route-specific codes are listed on each endpoint's page:
[images](/api-reference/images#errors-specific-to-these-routes),
[video](/api-reference/video#errors-specific-to-this-route),
[audio](/api-reference/audio#errors-specific-to-these-routes),
[chat](/api-reference/chat#errors), [Gemini](/api-reference/gemini#errors),
[jobs](/api-reference/jobs#job-read-errors).

## Failover, and what it means for you [#failover-and-what-it-means-for-you]

A request that fails on one upstream is retried on another before you see an error, so a `502` means
every candidate failed, not just the first. Two rules bound it:

* a **streamed** response never fails over once the first byte is on the wire;
* a **client error** is never retried, because retrying a malformed request only multiplies load.

This is invisible in the response except for the latency it can add. Set generous client timeouts on
`/v1/chat/completions` — the default 10 seconds in some HTTP clients is not enough for a long
completion, let alone a failover.

## Health [#health]

One unauthenticated endpoint reports whether the gateway can serve at all:

```bash
curl https://hypit.ai/healthz
```

```json
{
  "status": "ok",
  "version": "v0.4.2",
  "db": "ok",
  "outbox_pending": 0,
  "cleanup_running": true,
  "cleanup_pending": 12,
  "cleanup_oldest_due_age_s": 0
}
```

`status` and `db` are the two fields worth reading — a degraded gateway answers `503`. The
`outbox_*` and `cleanup_*` counters are operational and may change without notice. Use this for a
load-balancer probe or a status page, not as a per-request preflight.