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

> ## Agent Instructions
> For the complete API reference (every endpoint, query parameter and response field), fetch https://api.thestatsapi.com/llms.txt. Only use endpoints, parameters and fields listed there.
> Base URL: https://api.thestatsapi.com/api. Auth header: Authorization: Bearer YOUR_API_KEY. Keep the key in server-side code only.

# Rate limits and quotas

> How the per-minute limit and monthly quota work, how to read the rate-limit headers, and how to handle 429 responses with Retry-After.

Use this page to keep your app inside your plan's limits and recover cleanly when you hit one.

<Tip>
  **Fastest way: ask your AI.** Set up your coding tool once with [Build with AI](/docs/build-with-ai), then paste:

  ```text theme={null}
  Using TheStatsAPI (reference: https://api.thestatsapi.com/llms.txt), write a server-side fetch wrapper that caches responses, and on a 429 with code RATE_LIMITED waits the Retry-After seconds and retries up to 3 times. On USAGE_LIMIT_EXCEEDED it must stop and report the error, not retry.
  ```
</Tip>

## Two budgets

Every API key works within two separate budgets.

| Budget | Resets | Counted per | When it runs out |
| - | - | - | - |
| Per-minute rate limit | At the start of each minute | API key | `429 RATE_LIMITED` |
| Monthly quota | When your billing cycle rolls over | Account | `429 USAGE_LIMIT_EXCEEDED` |

The numbers depend on your plan. Some plans have no monthly cap. See [pricing](https://www.thestatsapi.com/#pricing).

* **Trials** are metered at 10% of the plan's limits.
* **Every request counts once your key is accepted**, including requests that return an error such as `400` or `404`.
* **`GET /health`** needs no key and doesn't count.

## Read the headers

Every authenticated response, success or error, includes these headers:

| Header | Meaning |
| - | - |
| `X-RateLimit-Limit` | Requests allowed per minute. |
| `X-RateLimit-Remaining` | Requests left this minute, counting this one. `0` means your next request is rejected. |
| `X-RateLimit-Reset` | Unix time, in seconds, when the minute resets. |
| `X-Monthly-Quota-Limit` | Requests allowed in this billing cycle. `-1` means no monthly cap. |
| `X-Monthly-Quota-Remaining` | Requests left in this billing cycle, counting this one. `-1` means no monthly cap. |
| `X-Monthly-Quota-Reset` | Unix time, in seconds, when your billing cycle rolls over. |
| `Retry-After` | Only on `429` responses. Seconds to wait before retrying. |

To see them, add `-i` to any request:

<CodeGroup>
  ```bash curl theme={null}
  curl -i "https://api.thestatsapi.com/api/football/competitions?per_page=1" \
    -H "Authorization: Bearer YOUR_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch("https://api.thestatsapi.com/api/football/competitions?per_page=1", {
    headers: { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` },
  });

  console.log({
    perMinuteLeft: res.headers.get("X-RateLimit-Remaining"),
    monthlyLeft: res.headers.get("X-Monthly-Quota-Remaining"), // "-1" = no monthly cap
  });
  ```
</CodeGroup>

## When you hit a limit

Both limits return `429` with a `Retry-After` header. Check the error `code` to know which one you hit.

| Code | What `Retry-After` holds | What to do |
| - | - | - |
| `RATE_LIMITED` | Seconds until the current minute resets, from 1 to 60. | Wait that long, then retry. |
| `USAGE_LIMIT_EXCEEDED` | Seconds until your billing cycle rolls over. This is days, not seconds. | Don't retry in a loop: every retry gets another `429`. Upgrade on the [pricing page](https://www.thestatsapi.com/#pricing) or wait for the new cycle. |

```json theme={null}
{
  "error": {
    "code": "USAGE_LIMIT_EXCEEDED",
    "message": "Monthly usage limit exceeded. Please upgrade your plan.",
    "status_code": 429
  }
}
```

## Retry with backoff

This wrapper retries `RATE_LIMITED` after `Retry-After` seconds and gives up straight away on `USAGE_LIMIT_EXCEEDED`.

<CodeGroup>
  ```javascript JavaScript theme={null}
  const BASE_URL = "https://api.thestatsapi.com/api";
  const headers = { Authorization: `Bearer ${process.env.THESTATSAPI_API_KEY}` };
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

  async function getWithRetry(path, maxRetries = 3) {
    for (let attempt = 0; ; attempt++) {
      const res = await fetch(`${BASE_URL}${path}`, { headers });
      const body = await res.json();

      if (res.ok) return body;

      const { code, message } = body.error;
      const canRetry = code === "RATE_LIMITED" && attempt < maxRetries;
      if (!canRetry) throw new Error(`${res.status} ${code}: ${message}`);

      const waitSeconds = Number(res.headers.get("Retry-After")) || 1;
      await sleep(waitSeconds * 1000);
    }
  }

  const { data } = await getWithRetry("/football/competitions/comp_3039");
  console.log(data.name);
  ```

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

  BASE_URL = "https://api.thestatsapi.com/api"
  HEADERS = {"Authorization": f"Bearer {os.environ['THESTATSAPI_API_KEY']}"}

  def get_with_retry(path, params=None, max_retries=3):
      attempt = 0
      while True:
          res = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
          body = res.json()

          if res.ok:
              return body

          error = body["error"]
          can_retry = error["code"] == "RATE_LIMITED" and attempt < max_retries
          if not can_retry:
              raise RuntimeError(f"{res.status_code} {error['code']}: {error['message']}")

          time.sleep(int(res.headers.get("Retry-After", "1")))
          attempt += 1

  data = get_with_retry("/football/competitions/comp_3039")["data"]
  print(data["name"])
  ```
</CodeGroup>

<Tip>
  To avoid the `429` in the first place, check `X-RateLimit-Remaining`. When it reaches `0`, wait until `X-RateLimit-Reset` before the next request.
</Tip>

## Use fewer requests

* **Call the API from your server and cache the responses.** One cached response can serve all your users. Calling from the browser also exposes your key.
* **Cache slow-changing data for longer**, such as competitions, seasons and finished matches.
* **Poll live endpoints only while a match is live**, and stop at full time. The API has no webhooks or push updates, so polling is the only way to get new data.
* **Fetch 100 results per page** (`per_page=100`) and filter on the server with parameters like `team_id`, `date_from` and `date_to`. See [Pagination](/docs/concepts/pagination).
* **Check availability flags** such as `xg_available` and `odds_available` before calling optional endpoints. See [Data model](/docs/concepts/data-model#availability-flags).

<Warning>
  AI coding agents can use up quota fast when they run code in a loop. Add a line like this to your prompt: "Make one test request first, cache every response while you build, and never call TheStatsAPI in a tight loop."
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.