> For AI assistants: read [llms.txt](https://docs.saturation.io/llms.txt) first for the complete documentation map, API contract, and endpoint Markdown files.

# Rate limits

The API returns `429` with code `rate_limited` when you exceed the request rate. Read `Retry-After`, wait that many seconds, then retry.

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{
  "success": false,
  "code": "rate_limited",
  "message": "Rate limit exceeded.",
  "requestId": "req_9c1d2e3f",
  "retryAfter": 12
}
```

## What you can rely on

- A `429` always carries a `Retry-After` header, an integer number of **seconds** to wait before you retry.
- The same value comes back in the body as `retryAfter`.
- `rate_limited` is safe to retry once `Retry-After` has elapsed.
- When an operation requires `Idempotency-Key`, reuse that key for its retry. See [Idempotency](/idempotency.md).

The numeric limits are unpublished and can change, so don't hard-code a rate.

## Backing off correctly

```ts
async function callWithBackoff(req: () => Promise<Response>): Promise<Response> {
  for (;;) {
    const res = await req();
    if (res.status !== 429) return res;

    const retryAfterHeader = res.headers.get('Retry-After');
    if (!retryAfterHeader) throw new Error('429 response missing Retry-After');

    const retryAfter = Number(retryAfterHeader);
    if (!Number.isInteger(retryAfter) || retryAfter < 0) {
      throw new Error('Retry-After must be a non-negative integer number of seconds');
    }

    await new Promise((r) => setTimeout(r, retryAfter * 1000));
  }
}
```

Expose `code === 'rate_limited'` and the parsed `retryAfter` from your client so callers can apply the same wait in `catch`.

## Next

- [Errors](/errors.md): the full status catalog and retry policy.
- [Idempotency](/idempotency.md): safe retries for `POST` creates.
