> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://statpad.dev/docs/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://statpad.dev/docs/_mcp/server.

# Rate limits

Every API key on your account shares two limits:

* **A request rate.** It works like a token bucket: you can send your plan's burst all at once, and it refills at your plan's sustained rate.
* **A monthly allowance.** This is the total number of requests your plan includes for each billing period.

Only successful responses (`2xx` and `304`) count against the allowance. Errors, including `403`, `404` and `429`, are free.

## When you hit a limit

Both limits answer with `429 Too Many Requests`, a `Retry-After` header in seconds, and no data. The problem `code` says which limit you hit:

| `code`            | What happened                                                           | What to do                                          |
| ----------------- | ----------------------------------------------------------------------- | --------------------------------------------------- |
| `rate_limited`    | You sent requests faster than your plan's rate.                         | Wait `Retry-After` seconds, then slow down.         |
| `quota_exhausted` | You've used this period's allowance. The response includes `resets_at`. | Wait for the period to reset, or upgrade your plan. |

If the API briefly can't check your account or its limits, it returns `503` with code `unavailable` rather than serving the request unmetered. Wait `Retry-After` seconds and try again.

## Usage headers

Every response that counts toward your limits carries two headers, including errors and `429`s. They follow the IETF [RateLimit header fields draft](https://datatracker.ietf.org/doc/html/draft-ietf-httpapi-ratelimit-headers-11).

```http
RateLimit-Policy: "allowance";q=500000;w=2592000
RateLimit: "allowance";r=499120;t=1723456
```

| Header             | Field | Meaning                                                                    |
| ------------------ | ----- | -------------------------------------------------------------------------- |
| `RateLimit-Policy` | `q`   | Requests your plan allows per period.                                      |
|                    | `w`   | Length of the current period, in seconds.                                  |
| `RateLimit`        | `r`   | Requests left this period, after this response.                            |
|                    | `t`   | Whole seconds, rounded up, until the period resets and `r` returns to `q`. |

Some things to keep in mind:

* **`r` is a snapshot.** A response that doesn't count, such as an error, reports `r` unchanged. Your other keys draw from the same allowance, so the next response can report less than you expect.
* **`Retry-After` wins.** On `quota_exhausted`, `r` is `0` and `t` equals `Retry-After`. On `rate_limited`, `r` can still be positive, because the rate limit isn't reported in these headers. Follow `Retry-After` either way.
* **Some responses don't carry the headers.** A `401`, a `403` for an account without active access, a `503`, or a request from a plan without REST access is rejected before it's counted.
* **Parse the headers leniently.** They're [Structured Field](https://www.rfc-editor.org/rfc/rfc9651) lists. Ignore policies and parameters you don't recognize, because more may be added.

## Reading the headers

```ts
const res = await fetch("https://api.statpad.dev/v1/leagues", {
  headers: { Authorization: `Bearer ${process.env.STATPAD_API_KEY}` },
});

if (res.status === 429 || res.status === 503) {
  const wait = Number(res.headers.get("Retry-After"));
  // Wait `wait` seconds before the next request.
}

const left = res.headers.get("RateLimit")?.match(/;r=(\d+)/)?.[1];
if (left !== undefined) console.log(`${left} requests left this period`);
```