# Rate Limits

Per-IP and per-endpoint request limits, the headers that expose them, and how to retry safely

The API enforces two layers of rate limiting: a global per-IP budget that protects every route,
and tighter windows on auth-critical endpoints like sign-in and two-factor verification. Both
layers return `429 Too Many Requests` with a stable error code and standard headers you can read
on every response.

## Global limit

A single IP may issue up to **500 requests per 15 minutes** across all routes combined.

| Window     | Max requests | Scope          |
| ---------- | ------------ | -------------- |
| 15 minutes | 500          | Per IP, global |

## Per-endpoint auth limits

On top of the global budget, auth-critical endpoints have stricter windows to prevent credential stuffing and brute-force.

| Endpoint                  | Max attempts | Window     |
| ------------------------- | ------------ | ---------- |
| `/api/auth/sign-in/email` | 5            | 60 seconds |
| `/api/auth/sign-up/email` | 3            | 60 seconds |
| `/api/auth/two-factor/*`  | 5            | 60 seconds |
| `/api/auth/passkey/*`     | 10           | 60 seconds |
| `/api/auth/magic-link/*`  | 5            | 1 hour     |

These endpoints are throttled by the auth layer, which responds differently from the rest of the
API: the body is a bare `{"message": "Too many requests. Please try again later."}` — **no
envelope and no `code` field** — and the wait time arrives in an **`X-Retry-After`** header
(seconds), not `Retry-After`. Wait for the window to expire — there is no retry-with-backoff
strategy that beats the limit.

## Response headers

Every response carries standard rate-limit headers.

| Header                | Description                                                                    |
| --------------------- | ------------------------------------------------------------------------------ |
| `RateLimit-Limit`     | Maximum requests permitted in the current window                               |
| `RateLimit-Remaining` | Requests remaining in the current window                                       |
| `RateLimit-Reset`     | Seconds until the window resets                                                |
| `Retry-After`         | Present on global-limit `429` responses — seconds to wait before retrying      |
| `X-Retry-After`       | Present on `429`s from the `/api/auth/*` endpoints listed above — same meaning |

```text
HTTP/1.1 200 OK
RateLimit-Limit: 500
RateLimit-Remaining: 482
RateLimit-Reset: 612
Content-Type: application/json; charset=UTF-8

{
"success": true,
"status": 200,
"code": "OK",
"message": "Projects fetched successfully",
"data": [...]
}
```

## `429` response

When you exceed the global limit, the API returns:

```json
{
"success": false,
"status": 429,
"code": "RATE_LIMIT_ERROR",
"message": "Too many requests from this IP, please try again later.",
"meta": {
  "message": "Too many requests from this IP, please try again later."
}
}
```

The accompanying `Retry-After` header tells you how many seconds to wait. Honour it.

When a per-endpoint auth limit trips instead, the response is not the envelope:

```text
HTTP/1.1 429 Too Many Requests
X-Retry-After: 42
Content-Type: application/json

{"message": "Too many requests. Please try again later."}
```

## Retry with backoff

Use exponential backoff for `429` and transient `5xx` responses. Read `Retry-After` first; fall back to `2^attempt` seconds if the header is missing.

**JavaScript**

```javascript
async function requestWithBackoff(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
  const res = await fetch(url, options);

  if (res.status !== 429 && res.status < 500) return res;
  if (attempt === maxRetries) return res;

  const retryAfter = Number(res.headers.get('Retry-After') ?? res.headers.get('X-Retry-After')) || Math.pow(2, attempt);
  await new Promise((r) => setTimeout(r, retryAfter * 1000));

}
}
```

**Python**

```python
import time, requests

def request_with_backoff(method, url, max_retries=3, **kwargs):
for attempt in range(max_retries + 1):
res = requests.request(method, url, **kwargs)

      if res.status_code != 429 and res.status_code < 500:
          return res
      if attempt == max_retries:
          return res

      retry_after = int(res.headers.get('Retry-After') or res.headers.get('X-Retry-After') or pow(2, attempt))
      time.sleep(retry_after)
```

## Staying under the limit

- **Cache reads:** Cache responses whose underlying data changes slowly (reference data, organization settings, user profile).
- **Batch writes when possible:** Use endpoints that accept arrays rather than calling a single-resource endpoint in a loop.
- **Poll sparingly:** For long-running work, poll every few seconds rather than every few milliseconds.
- **Use webhooks when available:** Subscribing to provider webhooks is far cheaper than polling for state changes.
- **Observe your budget:** Log `RateLimit-Remaining` on hot paths; a low floor is a signal to back off preemptively.

## Need higher limits?

If your integration legitimately needs more headroom, contact your account manager with details about your use case and traffic pattern.

## Best Practices

- **Read `Retry-After` before guessing a delay:** The server knows exactly when the window resets. Your backoff math is an approximation.
- **Never retry sign-in or 2FA on `429`:** These use credential-safety limits. Retrying only extends the lockout.
- **Log `code === 'RATE_LIMIT_ERROR'` separately:** Distinguish rate limits from generic `4xx` so you can spot noisy callers.
- **Distribute traffic across IPs where legitimate:** The limiter is per-IP. A single bot from one IP will hit the cap long before a fleet of workers across a NAT pool.
- **Do not rotate IPs to dodge the limit:** That is treated as abuse and can get the account suspended.

## Next Steps

1. **Handle errors:** Review [Error Handling](/docs/get-started/errors) for the full list of codes including `RATE_LIMIT_ERROR`.
2. **Scope down:** Confirm your [Scopes & Permissions](/docs/get-started/scopes) before you ship anything that runs in a tight loop.
3. **Plan for SSO:** If you administer an organization, set up [Enterprise SSO](/docs/get-started/enterprise-sso).
4. **Explore endpoints:** Jump into the [API Reference](/docs/api-reference/api-keys).