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

# Rate limits

> Each key and each workspace has a budget of requests a minute, a report page counts as several requests, and every response tells you how much is left.

## Two budgets, both counted

Every request spends from two budgets at once:

* **the key's own**, so one runaway script cannot use up everything;
* **the workspace's**, shared by every key in the workspace and every MCP sign-in connection, so ten keys cannot multiply the workspace's ceiling.

Both are counted on every request, and a request is refused when either is empty. Each key's line on **Workspace settings → API keys** shows its own limit.

| Limit | Value |
| - | - |
| Requests a minute, per API key | 60 |
| Requests a minute, per workspace (every key together) | 120 |
| Requests a minute, per signed-in app (Claude, ChatGPT) | 60 |
| Rows per page, lists | 100 |
| Rows per page, reports | 200 |
| What one report page counts as, against the limits above | 10 requests |
| Brands report without `date`: the most prompt-engine-days | 18,000 (9,000 with `segment`) |
| Domains and URLs reports without `date`: the most prompt-engine-days | 4,200 (2,100 with `segment`) |
| The longest window, with `date` as a dimension | 366 days |
| Report rows × prompts in scope, per page | 20,000 |

There is also a ceiling per calling address on the `/v1` routes. It sits well above a key's budget, so a single job meets the key and workspace budgets first.

## Report pages weigh more

A page of the [brands, domains or URLs report](https://docs.citino.ai/api/reports.md) costs about as much to build as several ordinary reads, so it counts as several requests against both budgets. The table above gives the weight. The MCP report tools count the same as their REST routes, and the `RateLimit-*` headers on the response include the whole weight. An MCP report tool that would go over a budget, or that is refused as `busy` (below), does not get an HTTP `429` or `Retry-After`: it answers with an error result whose text starts `rate_limited` or `busy`.

## One report at a time per workspace

Run report requests one after another, not side by side. While one of your workspace's report requests is running, a second one gets `503` with the code `busy` at once. When the API is already building as many reports as it allows for everyone, a new request waits briefly and then gets `busy` too. `busy` is not a rate limit: your budgets are untouched, and the `Retry-After` header says how many seconds to wait before trying again. (A `503` with the code `query_timeout` is different: the request asked for too much at once, and retrying it unchanged is likely to time out again. See [Errors](https://docs.citino.ai/api/errors.md).)

## Reading your budget

Every authenticated response carries:

| Header | What it says |
| - | - |
| `RateLimit-Limit` | The credential's limit a minute (the key's, or the sign-in connection's) |
| `RateLimit-Remaining` | Requests left this minute: the lower of the key's and the workspace's |
| `RateLimit-Reset` | Seconds until the budget refills |
| `Citino-Api-Version` | The API version that answered |

A refused request is `429` with the code `rate_limited`, and a `Retry-After` header giving the seconds to wait.

```bash curl theme={null}
curl -i https://api.citino.ai/v1/projects -H "Authorization: Bearer $CITINO_API_KEY"
```

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

key = os.environ["CITINO_API_KEY"]
session = requests.Session()
session.headers["Authorization"] = f"Bearer {key}"


def retryable(response):
    if response.status_code == 429:
        return True
    return response.status_code == 503 and response.json()["error"]["code"] == "busy"


def get(url, **params):
    """GET, waiting out a 429 or a busy report as the server asks."""
    while True:
        response = session.get(url, params=params)
        if retryable(response):
            time.sleep(int(response.headers.get("Retry-After", "5")))
            continue
        response.raise_for_status()
        return response


page = get("https://api.citino.ai/v1/projects")
print("left this minute:", page.headers["RateLimit-Remaining"])
```

```javascript JavaScript theme={null}
const headers = { Authorization: `Bearer ${process.env.CITINO_API_KEY}` };
const sleep = (seconds) => new Promise((r) => setTimeout(r, seconds * 1000));

async function retryable(response) {
  if (response.status === 429) return true;
  if (response.status !== 503) return false;
  const { error } = await response.clone().json();
  return error.code === "busy";
}

// GET, waiting out a 429 or a busy report as the server asks.
async function get(url) {
  for (;;) {
    const response = await fetch(url, { headers });
    if (await retryable(response)) {
      await sleep(Number(response.headers.get("Retry-After") ?? "5"));
      continue;
    }
    if (!response.ok) throw new Error(`Citino answered ${response.status}`);
    return response;
  }
}

const page = await get("https://api.citino.ai/v1/projects");
console.log("left this minute:", page.headers.get("RateLimit-Remaining"));
```

> [!TIP]
>
> Pace yourself from `RateLimit-Remaining` rather than waiting to be refused. A nightly job that reads one day at a time stays inside every budget: see [Pulling data nightly](https://docs.citino.ai/api/nightly-pull.md).

## Limits only widen

Every limit in the table is a floor. Inside v1 a limit may be raised and is never lowered, so a job that fits today keeps fitting. The weight of a report page is a cost rather than a limit, so it is not part of that promise. See [Versioning](https://docs.citino.ai/api/versioning.md).

## Related

> [card] [Reports](https://docs.citino.ai/api/reports.md)
>
> Budgets in prompt-engine-days, and paging.

> [card] [Errors](https://docs.citino.ai/api/errors.md)
>
> `rate_limited`, `busy` and `query_timeout`.

> [card] [Pagination](https://docs.citino.ai/api/pagination.md)
>
> Cursors and page sizes.

> [card] [Pulling data nightly](https://docs.citino.ai/api/nightly-pull.md)
>
> A daily job that fits on every plan.


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