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

# Errors

> Every error code, when it fires, and how to handle it

When a request fails, the API returns a non-2xx HTTP status and an
error envelope with a machine-readable `code` field, a human-readable
`message`, and (sometimes) a structured `details` payload.

```json theme={null}
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request parameters",
    "details": {
      "errors": [
        { "type": "literal_error", "loc": ["query", "symbol"],
          "msg": "Input should be 'BTCUSDT', 'ETHUSDT', 'SOLUSDT' or 'HYPEUSDT'" }
      ]
    }
  },
  "meta": {
    "version": "2.0",
    "timestamp": "2026-04-30T12:00:00Z",
    "request_id": "req_a1b2c3..."
  }
}
```

## All error codes

| Code                  | HTTP status     | When it fires                                                                             |
| --------------------- | --------------- | ----------------------------------------------------------------------------------------- |
| `UNAUTHORIZED`        | 401             | Missing, invalid, or revoked `X-API-Key` header.                                          |
| `FORBIDDEN`           | 403             | Key is valid but the subscription tier doesn't allow this resource.                       |
| `NOT_FOUND`           | 404             | Symbol, expiry, or other resource not present in cache or DB.                             |
| `VALIDATION_ERROR`    | 400 / 422       | Bad query parameter — out of range, wrong type, or unknown enum value.                    |
| `RATE_LIMIT_EXCEEDED` | 429             | Per-tier rate limit hit. Retry after `meta.rate_limit.reset` or the `Retry-After` header. |
| `UPSTREAM_ERROR`      | 502 / 503 / 504 | Cache (Redis) or database (Postgres) temporarily unreachable. Retry with backoff.         |
| `INTERNAL_ERROR`      | 500             | Unexpected server-side failure. Include `meta.request_id` when reporting.                 |

The full list is also available programmatically at
[`/v2/meta`](/api/v2/discovery/meta) — useful if you want to keep your
client's error map in sync with the API.

## Handling errors in code

Always check `success` before reading `data`. Use `error.code` to branch
your error handling.

<CodeGroup>
  ```python Python theme={null}
  import requests
  import time

  def get_gex_levels(api_key, symbol="BTCUSDT", max_retries=3):
      for attempt in range(max_retries):
          resp = requests.get(
              "https://api.backquant.com/v2/gex/levels",
              params={"symbol": symbol},
              headers={"X-API-Key": api_key},
          )
          body = resp.json()

          if body["success"]:
              return body["data"]

          code = body["error"]["code"]

          if code == "UNAUTHORIZED":
              raise ValueError(
                  "Invalid API key. Get/rotate at https://backquant.com/api-access"
              )
          if code == "RATE_LIMIT_EXCEEDED":
              retry_after = int(resp.headers.get("Retry-After", "60"))
              print(f"Rate limited; sleeping {retry_after}s")
              time.sleep(retry_after)
              continue
          if code == "NOT_FOUND":
              # 404 = no cache for this symbol/expiry. Don't retry — handle as no-data.
              return None
          if code == "UPSTREAM_ERROR":
              # Redis/DB blip — exponential backoff
              sleep = 2 ** attempt
              print(f"Upstream error; sleeping {sleep}s")
              time.sleep(sleep)
              continue
          if code == "VALIDATION_ERROR":
              raise ValueError(f"Invalid parameters: {body['error']}")

          raise RuntimeError(
              f"Unexpected error: {body['error']} "
              f"(request_id={body['meta'].get('request_id')})"
          )
      raise RuntimeError("Max retries exceeded")
  ```

  ```typescript TypeScript theme={null}
  async function getGexLevels(
    apiKey: string,
    symbol = "BTCUSDT",
    maxRetries = 3,
  ): Promise<unknown | null> {
    for (let attempt = 0; attempt < maxRetries; attempt++) {
      const resp = await fetch(
        `https://api.backquant.com/v2/gex/levels?symbol=${symbol}`,
        { headers: { "X-API-Key": apiKey } },
      );
      const body = await resp.json();

      if (body.success) return body.data;

      const code = body.error.code;

      if (code === "UNAUTHORIZED") {
        throw new Error(
          "Invalid API key. Get/rotate at https://backquant.com/api-access",
        );
      }
      if (code === "RATE_LIMIT_EXCEEDED") {
        const retryAfter = parseInt(resp.headers.get("Retry-After") ?? "60", 10);
        console.log(`Rate limited; sleeping ${retryAfter}s`);
        await new Promise((r) => setTimeout(r, retryAfter * 1000));
        continue;
      }
      if (code === "NOT_FOUND") {
        return null; // no data, don't retry
      }
      if (code === "UPSTREAM_ERROR") {
        const sleep = 2 ** attempt;
        console.log(`Upstream error; sleeping ${sleep}s`);
        await new Promise((r) => setTimeout(r, sleep * 1000));
        continue;
      }
      if (code === "VALIDATION_ERROR") {
        throw new Error(`Invalid parameters: ${JSON.stringify(body.error)}`);
      }
      throw new Error(
        `Unexpected error: ${body.error.message} ` +
          `(request_id=${body.meta?.request_id})`,
      );
    }
    throw new Error("Max retries exceeded");
  }
  ```
</CodeGroup>

## Guidance per code

**`UNAUTHORIZED`** — Check the `X-API-Key` header is present, the value
has no leading/trailing space, and the key hasn't been revoked at
[backquant.com/api-access](https://backquant.com/api-access).

**`FORBIDDEN`** — Your subscription doesn't allow this resource.
Usually a sign you've downgraded a paid tier or your subscription
lapsed.

**`NOT_FOUND`** — Don't retry. Either the symbol/expiry doesn't have
cache yet (cold start, or unsupported symbol), or the path is wrong.

**`VALIDATION_ERROR`** — Read `error.details.errors` for the failing
parameter list. Common causes: passing `BTC` instead of `BTCUSDT`,
unsupported `?greek=` value, out-of-range numeric param.

**`RATE_LIMIT_EXCEEDED`** — Read `Retry-After` header (or
`meta.rate_limit.reset`) and wait. The
[`/v2/options/chain`](/api/v2/options/chain) is the most expensive
single endpoint — if you're polling it aggressively, batch with
[multi-symbol bundles](/concepts/multi-symbol) or pull only the
contracts you need with `?max_contracts`.

**`UPSTREAM_ERROR`** — Transient. Exponential backoff (2s, 4s, 8s)
usually clears it within \~30s. If it persists past a minute, check
[`/v2/health`](/api/v2/health).

**`INTERNAL_ERROR`** — Always include `meta.request_id` when reporting
to [dev@backquant.com](mailto:dev@backquant.com). We trace by it.

## Always-present headers

Even on errors, the response carries:

* `X-RateLimit-Limit` — your tier's per-minute cap
* `X-RateLimit-Remaining` — calls left in the current window
* `X-RateLimit-Reset` — Unix timestamp when the window resets
* `Retry-After` — seconds until safe to retry (on 429 only)
* `X-Request-ID` — the same UUID as `meta.request_id`

## See also

<CardGroup cols={2}>
  <Card title="Rate limits" icon="gauge" href="/concepts/rate-limits">
    Per-tier limits and how to think about backoff.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication">
    Avoiding `UNAUTHORIZED` errors.
  </Card>

  <Card title="Data freshness" icon="bolt" href="/concepts/data-freshness">
    What `NOT_FOUND` actually means per endpoint.
  </Card>
</CardGroup>
