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

# Limits & Errors

> Data API rate limits, rate-limit headers, 429 semantics and backoff, the full error-code table, the 90-day history window, and what rotation and revocation do to a key.

**60 requests per minute and 2,000 requests per day, per key.** Both windows are metered per key, not per IP, so two keys never spend each other's quota. A separate per-address throttle sits in front of key resolution at ten times the per-key rate – see [`ip_rate_limited`](#ip-rate-limited). Calls made through the [MCP server](/data-access/mcp-server) draw on the same two windows.

A poll every 30 minutes spends 48 requests a day. Sustained production volume needs a license – [Index Services](https://beliefsystems.xyz/license).

Those two numbers describe the free tier. `GET /v1` reports the limits actually in force for your key as `limitMinute` and `limitDay` in its `key` block, beside `remainingMinute` and `remainingDay`, and the rate-limit headers below carry the same figures. Read the `key` block, not the `limits` block, whenever the two disagree.

## Headers

The six `X-RateLimit-*` headers are on every authenticated response, success and error alike. The two pre-authentication rejections, [`invalid_api_key`](#invalid-api-key) and [`ip_rate_limited`](#ip-rate-limited), have no key to report on and carry none of them. `X-Request-Id` is on every response, including those two.

| Header                      | Value                                              |
| --------------------------- | -------------------------------------------------- |
| `X-RateLimit-Limit`         | Requests allowed in the minute window              |
| `X-RateLimit-Remaining`     | Requests left in the current minute window         |
| `X-RateLimit-Limit-Day`     | Requests allowed in the day window                 |
| `X-RateLimit-Remaining-Day` | Requests left in the current day window            |
| `X-RateLimit-Reset`         | Unix seconds at which the minute window resets     |
| `X-RateLimit-Reset-Day`     | Unix seconds at which the day window resets        |
| `X-Request-Id`              | Correlation id for this request, on every response |
| `Retry-After`               | Seconds to wait. Sent on `429` only                |

Quote `X-Request-Id` in any support message. It is what turns "I got a 500 around 14:02" into a single log lookup.

## When you are throttled

A rejected request returns `429` with `Retry-After` and an error code that names which limit you hit: [`rate_limited`](#rate-limited) for the minute window, [`daily_limit_reached`](#daily-limit-reached) for the day window, [`ip_rate_limited`](#ip-rate-limited) for the pre-authentication throttle.

Sleep for `Retry-After` rather than a constant. It is exact on both windows, computed from the moment your oldest counted request ages out, and it agrees to the second with `X-RateLimit-Reset` and `X-RateLimit-Reset-Day`.

<CodeGroup>
  ```bash curl theme={null}
  url="https://api.beliefsystems.xyz/v1/indices/CONFLICT/latest"

  for attempt in 1 2 3; do
    body=$(curl -s -D /tmp/belief-headers -H "Authorization: Bearer $BELIEF_API_KEY" "$url")
    status=$(awk 'NR==1 {print $2}' /tmp/belief-headers)

    if [ "$status" != "429" ]; then
      echo "$body" | jq '.data'
      break
    fi

    sleep "$(grep -i '^retry-after:' /tmp/belief-headers | tr -dc '0-9')"
  done
  ```

  ```python Python theme={null}
  import os
  import time

  import requests

  HEADERS = {"Authorization": f"Bearer {os.environ['BELIEF_API_KEY']}"}


  def get_with_backoff(url, attempts=3):
      for _ in range(attempts):
          resp = requests.get(url, headers=HEADERS, timeout=10)
          if resp.status_code != 429:
              resp.raise_for_status()
              return resp.json()["data"]
          time.sleep(int(resp.headers.get("Retry-After", 60)))
      raise RuntimeError("Rate limited on every attempt")


  print(get_with_backoff("https://api.beliefsystems.xyz/v1/indices/CONFLICT/latest"))
  ```

  ```javascript JavaScript theme={null}
  async function getWithBackoff(url, attempts = 3) {
    for (let attempt = 0; attempt < attempts; attempt++) {
      const res = await fetch(url, {
        headers: { Authorization: `Bearer ${process.env.BELIEF_API_KEY}` },
      });

      if (res.status !== 429) {
        if (!res.ok) throw new Error(`Belief Systems API ${res.status}`);
        const { data } = await res.json();
        return data;
      }

      const wait = Number(res.headers.get("Retry-After") ?? 60);
      await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    }
    throw new Error("Rate limited on every attempt");
  }

  console.log(await getWithBackoff("https://api.beliefsystems.xyz/v1/indices/CONFLICT/latest"));
  ```
</CodeGroup>

## Error shape

Failures replace `data` and `meta` with a single `error` object. The `code` is the stable contract – match on it, not on the message, which is written for a person and may be reworded.

```json theme={null}
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded: 60 requests per minute per key.",
    "docsUrl": "https://docs.beliefsystems.xyz/api-reference/limits#rate-limited",
    "requestId": "9f2c1ad4e6b74c0f8a3d5e1b7c904a26"
  }
}
```

`docsUrl` deep links to the section for that code on this page. `requestId` matches the `X-Request-Id` header.

## Error codes

| Code                                          | Status | Meaning                                                                                              |
| --------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------- |
| [`invalid_api_key`](#invalid-api-key)         | 401    | No key, or a key that no longer resolves                                                             |
| [`ip_rate_limited`](#ip-rate-limited)         | 429    | Too many unauthenticated requests from one address                                                   |
| [`rate_limited`](#rate-limited)               | 429    | The per-key minute window is spent                                                                   |
| [`daily_limit_reached`](#daily-limit-reached) | 429    | The per-key day window is spent                                                                      |
| [`not_found`](#not-found)                     | 404    | No such index or series on this surface                                                              |
| [`method_not_allowed`](#method-not-allowed)   | 405    | The route exists; the HTTP method does not                                                           |
| [`invalid_parameter`](#invalid-parameter)     | 422    | A parameter failed validation                                                                        |
| [`invalid_interval`](#invalid-interval)       | 422    | `interval` was neither `1h` nor `1d`                                                                 |
| [`invalid_window`](#invalid-window)           | 422    | `start` was not before `end`                                                                         |
| [`internal_error`](#internal-error)           | 500    | A fault on our side                                                                                  |
| [`unavailable`](#unavailable)                 | 503    | The API is temporarily not serving                                                                   |
| `error`                                       | any    | Fallback for a status with no named code. Its `docsUrl` points at this page rather than at a section |

<h3 id="invalid-api-key">
  `invalid_api_key`
</h3>

The request carried no key, or one that has been revoked, rotated, or never existed. Check the `Authorization` header, then check the key list in the [portal](https://beliefsystems.xyz/data/portal). Keys are personal to an account and are not transferable.

<h3 id="ip-rate-limited">
  `ip_rate_limited`
</h3>

A single address sent too many requests before authenticating. It is an abuse shield in front of key resolution, sized far above the per-key limit, and valid keyed traffic counts toward it. Reaching it means one address is sending a great deal of traffic – most often a loop retrying a rejected key, occasionally many clients behind one address.

<h3 id="rate-limited">
  `rate_limited`
</h3>

The key spent its 60 requests for the current minute. `Retry-After` is exact here, and `X-RateLimit-Reset` is the same moment as unix seconds.

<h3 id="daily-limit-reached">
  `daily_limit_reached`
</h3>

The key spent its 2,000 requests for the current day window. `Retry-After` is exact here, and `X-RateLimit-Reset-Day` is the same moment as unix seconds. If a workload reaches this regularly, it has outgrown the free tier: [Index Services](https://beliefsystems.xyz/license) covers sustained and production volume.

<h3 id="not-found">
  `not_found`
</h3>

No index or volatility series matches that ticker on this surface. Unknown tickers, unlisted series, and series with no published values yet all return the same 404 – a ticker that is not on the API is not distinguishable from one that does not exist. `GET /v1/indices` and `GET /v1/volatility` are the catalogs.

<h3 id="method-not-allowed">
  `method_not_allowed`
</h3>

The path is a real endpoint, but not with that verb. `/v1` is read-only: every endpoint is `GET`. The response carries an `Allow` header naming the methods the route accepts.

<h3 id="invalid-parameter">
  `invalid_parameter`
</h3>

A query parameter failed validation – an unparseable date, a `limit` outside its range. The message names the parameter.

<h3 id="invalid-interval">
  `invalid_interval`
</h3>

`interval` must be `1h` or `1d`. See [Intervals and the history window](#intervals) below; finer granularity is part of the licensed feed.

<h3 id="invalid-window">
  `invalid_window`
</h3>

`start` was not before `end`. The message quotes both values as parsed, to the second, which is usually enough to spot a timezone or ordering mistake in your own input. An `end` in the future is named as such: history ends at now, so a future `end` is reported against the current time rather than silently accepted. A window that is merely older than the free range is not this error – it is clamped, not rejected.

<h3 id="internal-error">
  `internal_error`
</h3>

A fault on our side, not in your request. Retry with backoff. This is the one status where `X-Request-Id` does the real work: quote it and the failure is a single log lookup rather than a search.

<h3 id="unavailable">
  `unavailable`
</h3>

The API is temporarily not serving. Retry with backoff. Index levels remain published on [beliefsystems.xyz](https://beliefsystems.xyz/indices) throughout, and the snapshot bundles are static files served independently.

<h2 id="intervals">
  Intervals and the history window
</h2>

`GET /v1/indices/{ticker}/history` takes `start`, `end`, and `interval`. `interval` is `1h` or `1d`. History reaches back **90 days** from now.

All three are optional. With none supplied you get the full trailing window at `1d`.

A window that starts earlier is clamped, never rejected. A chart that asked for a year still renders; `meta` tells you exactly what it got:

| Field            | Meaning                                             |
| ---------------- | --------------------------------------------------- |
| `effectiveStart` | The start actually served, after clamping           |
| `effectiveEnd`   | The end actually served                             |
| `clamped`        | `true` when the requested start predated the window |
| `historyMaxDays` | The window in force, currently 90                   |

A request for `start=2025-12-01&end=2026-06-03&interval=1d`, made on August 30, 2026, reaches back past the floor. The window moves; the request does not fail.&#x20;

```json theme={null}
{
  "data": {
    "ticker": "CONFLICT",
    "interval": "1d",
    "count": 2,
    "points": [
      { "t": "2026-06-02T00:00:00.000000Z", "indexLevel": "52.31840975", "stale": false },
      { "t": "2026-06-03T00:00:00.000000Z", "indexLevel": "51.77204318", "stale": false }
    ]
  },
  "meta": {
    "attribution": "Source: Belief Systems (beliefsystems.xyz)",
    "license": "https://beliefsystems.xyz/license",
    "termsUrl": "https://beliefsystems.xyz/terms",
    "generatedAt": "2026-08-30T12:00:00.000000Z",
    "effectiveStart": "2026-06-01T12:00:00.000000Z",
    "effectiveEnd": "2026-06-03T00:00:00.000000Z",
    "clamped": true,
    "historyMaxDays": 90
  }
}
```

A window that falls entirely before the floor returns an empty `points` array with `clamped: true`, rather than an error. Read `clamped` before labeling a chart with the range you asked for.

Belief Volatility history clamps the same way, against the same 90-day floor, and echoes `effectiveStart`, `clamped`, and `historyMaxDays`.

Full-depth history since inception is in the free [snapshot bundles](/data-access/downloads).

## Keys

An account holds up to **three active keys**. The secret is displayed once, at creation, and is never retrievable afterward – the system stores a hash, not the key.

* **Rotation** issues a new secret and retires the old one in the same operation. The old secret stops working immediately; usage history stays with the account.
* **Revocation** takes effect immediately. Every subsequent call returns [`invalid_api_key`](#invalid-api-key).
* Snapshot file downloads validate keys against a one-minute cache, so a revoked key can still open a snapshot file for up to a minute after revocation. The API itself does not wait.
* Rate limits and history limits are access controls under the [Terms of Service](https://beliefsystems.xyz/terms). Distributing requests across several keys or accounts to exceed them is prohibited.

Both operations are in the [portal](https://beliefsystems.xyz/data/portal), alongside per-key usage.
