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

# Rate Limiting

> Request limits applied per tenant, using a sliding window

API requests are rate limited to **120 requests per minute** per organization/API key. Every API key and integration draws from this shared allowance. Limits are enforced automatically on all API endpoints.

## Sliding window

The limit is enforced with a **sliding window**, not a fixed one. There is no
clock boundary where the whole allowance resets at once — capacity returns
gradually as your own earlier requests age out of the trailing 60-second window.

This means you cannot spend your full allowance right at the end of one minute
and then spend it again right at the start of the next. If you send 120
requests at `00:00:50`, you do not get another 120 at `00:01:00` — the earliest
of those requests only ages out at `00:01:50`.

## Response headers

Every rate-limited response carries the current state of the window:

```
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 119
X-RateLimit-Reset: 1786715340
X-RateLimit-Scope: tenant
```

`X-RateLimit-Reset` is a Unix timestamp in seconds — the moment your oldest
request in the current window ages out and frees up one slot. Because the window
slides, this timestamp moves with your own traffic rather than sitting on a fixed
boundary.

`X-RateLimit-Scope` is `tenant` for authenticated requests, or `ip` for
unauthenticated ones.

## Handling `429 Too Many Requests`

Exceeding the limit returns `429` with a `Retry-After` header giving the seconds
until the window has capacity again:

```
Retry-After: 10
```

```json theme={null}
{
  "statusCode": 429,
  "message": "Organisation per-minute rate limit of 120 exceeded. Retry after 10 seconds.",
  "error": "RATE_LIMIT_EXCEEDED",
  "scope": "tenant",
  "limit": 120,
  "window": "per-minute",
  "retryAfter": 10,
  "path": "/api/v1/profile",
  "timestamp": "2026-08-14T12:29:00.000Z"
}
```

Match on `error: "RATE_LIMIT_EXCEEDED"` rather than parsing the message.
`retryAfter` is the same value as the `Retry-After` header, provided in the body
too so it survives if intermediate infrastructure strips response headers.

<Callout type="warning">
  Requests that get rejected still count. If you ignore `Retry-After` and keep
  sending, your window stays full and every request keeps returning `429` —
  waiting is what frees up capacity, not sending more.
</Callout>

## Staying under the limit

* **Read `X-RateLimit-Remaining`** and slow down before you hit zero rather than
  waiting for a `429`.
* **Honour `Retry-After`.** Because the window is sliding, sending again before
  it elapses will just be rejected again.
* **Paginate deliberately.** `pageSize` up to 100 on list endpoints fetches more
  per request than looping over small pages.
* **Coordinate across integrations.** Because the limit is per tenant, a batch
  job and your production traffic share one allowance — schedule bulk work
  off-peak rather than assuming a separate key buys separate capacity.

## Exempt endpoints

Health probes and internal service-to-service routes are not rate limited, since
throttling them would cause outages rather than prevent them.
