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

# Rate limits

> How Chataigne meters requests per key and surface, the X-RateLimit-* headers, 429 responses, and how to back off safely.

The Chataigne API limits how many requests a single key can make. The public `/v1` limit is currently **1000 requests per 60 seconds**. Limits are applied **per key, per surface** — your public `/v1` traffic and your Admin `/admin/v1` traffic are metered independently, and each key has its own budget.

Every response carries headers that tell you exactly where you stand, so you can pace requests proactively instead of waiting to be throttled.

<Note>
  Limits are enforced per key. Rotating a key or issuing additional keys does not raise your total throughput on a surface — each key carries its own independent budget against the same backend.
</Note>

## How metering works

* **Per key.** Each API key (`ch_org_…` or `ch_admin_…`) is metered separately. One noisy key never starves another.
* **Per surface.** The public surface (`https://server.chataigne.ai/v1`) and the Admin surface (`https://server.chataigne.ai/admin/v1`) have independent counters. Exhausting one does not affect the other.
* **60-second window.** The counter resets every 60 seconds. `X-RateLimit-Reset` tells you when the current window resets.

## Rate limit headers

Every response — successful or not — includes the following headers:

<ResponseField name="X-RateLimit-Limit" type="integer">
  The maximum number of requests permitted in the current window for this key and surface.
</ResponseField>

<ResponseField name="X-RateLimit-Remaining" type="integer">
  The number of requests remaining in the current window. When this reaches `0`, further requests are rejected with `429` until the window resets.
</ResponseField>

<ResponseField name="X-RateLimit-Reset" type="integer">
  The Unix timestamp (seconds) at which the current window resets and `X-RateLimit-Remaining` is replenished.
</ResponseField>

<ResponseField name="Retry-After" type="integer">
  **Only present on `429` responses.** The number of seconds to wait before retrying. Always honor this value.
</ResponseField>

Read these headers on every response and slow down before you hit zero — the cheapest rate limit is the one you never trip.

```http Example response headers theme={null}
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 987
X-RateLimit-Reset: 1748520000
X-Request-Id: req_8f2c4a1b9d
```

## When you're throttled (429)

When a key exceeds its budget for a surface, the API responds with `429 Too Many Requests`, a `Retry-After` header, and a [standard error body](/concepts/errors) of type `rate_limit_error`.

```http 429 response theme={null}
HTTP/1.1 429 Too Many Requests
Retry-After: 2
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748520002
X-Request-Id: req_3a7e0c5f21
Content-Type: application/json
```

```json Error body theme={null}
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Too many requests. Retry after 2 seconds.",
    "request_id": "req_3a7e0c5f21"
  }
}
```

<Warning>
  `Retry-After` is authoritative. Do not retry before the interval it specifies — retrying early consumes budget against the next window and prolongs throttling. Include the `request_id` from the error body when contacting support.
</Warning>

## Backoff guidance

Build retry logic into every client. The recommended strategy:

1. **Honor `Retry-After` first.** On a `429`, wait at least the number of seconds it specifies before retrying.
2. **Use exponential backoff with jitter** for other transient failures (e.g. `500 api_error`). Double the delay each attempt and add a small random offset to avoid synchronized retry storms across clients.
3. **Cap your retries.** Stop after a few attempts (e.g. 5) and surface the error rather than retrying indefinitely.
4. **Pace proactively.** Watch `X-RateLimit-Remaining` and throttle yourself as it approaches zero — don't wait for the `429`.
5. **Make retries safe.** For resource-creating `POST`s, send an [`Idempotency-Key`](/concepts/idempotency) so a retry never creates a duplicate.

<CodeGroup>
  ```bash curl theme={null}
  # Retry once, respecting the server's Retry-After interval.
  URL="https://server.chataigne.ai/v1/locations/loc_3kf9x2qd7m"

  response=$(curl -s -w "\n%{http_code}" "$URL" \
    -H "x-api-key: ch_org_live_2Yb8Qd4Rf1Tg" \
    -D /tmp/headers.txt)

  status=$(echo "$response" | tail -n1)

  if [ "$status" = "429" ]; then
    retry_after=$(grep -i '^retry-after:' /tmp/headers.txt | awk '{print $2}' | tr -d '\r')
    echo "Rate limited. Waiting ${retry_after}s before retry..."
    sleep "$retry_after"
    curl -s "$URL" -H "x-api-key: ch_org_live_2Yb8Qd4Rf1Tg"
  fi
  ```

  ```javascript Node.js theme={null}
  const BASE_URL = "https://server.chataigne.ai/v1";
  const API_KEY = process.env.CHATAIGNE_API_KEY; // ch_org_live_…

  // Fetch with Retry-After handling + exponential backoff and jitter.
  async function requestWithRetry(path, options = {}, maxRetries = 5) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      const res = await fetch(`${BASE_URL}${path}`, {
        ...options,
        headers: { "x-api-key": API_KEY, ...options.headers },
      });

      // Proactively slow down as the window nears exhaustion.
      const remaining = Number(res.headers.get("X-RateLimit-Remaining"));
      if (remaining <= 1) console.warn("Approaching rate limit; pace requests.");

      if (res.status === 429) {
        const retryAfter = Number(res.headers.get("Retry-After")) || 1;
        await sleep(retryAfter * 1000);
        continue;
      }

      if (res.status >= 500 && attempt < maxRetries) {
        const backoff = 2 ** attempt * 1000 + Math.random() * 250; // jitter
        await sleep(backoff);
        continue;
      }

      return res;
    }
    throw new Error("Exhausted retries");
  }

  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  // Example: read a special closing for a location.
  const res = await requestWithRetry("/locations/loc_3kf9x2qd7m/special_closings/scl_9wq2m4t8");
  const closing = await res.json();
  console.log(closing.id, "->", closing.object); // scl_9wq2m4t8 -> special_closing
  ```
</CodeGroup>

## Reducing your request volume

Before scaling up retries, scale down the number of calls you make:

<CardGroup cols={2}>
  <Card title="Expand related objects" icon="diagram-project" href="/concepts/expanding-objects">
    Use `expand[]` to inline related resources in a single request — e.g. `?expand[]=locations` — instead of fetching them one by one.
  </Card>

  <Card title="Paginate efficiently" icon="list" href="/concepts/pagination">
    Request larger pages (`limit` up to 100) and use cursor pagination (`starting_after`) to walk lists with fewer round trips.
  </Card>

  <Card title="Send idempotency keys" icon="key" href="/concepts/idempotency">
    Idempotency-protected retries return the original response without doing duplicate work — safe to retry after a `429`.
  </Card>

  <Card title="Use request IDs" icon="fingerprint" href="/concepts/request-ids">
    Capture `X-Request-Id` from throttled responses so support can trace any anomalous limiting quickly.
  </Card>
</CardGroup>

<Note>
  Need a higher limit for a specific integration? Reach out with your `org_…` id and a representative `request_id` so we can review your traffic pattern.
</Note>
