# Rate limits

Requests are rate-limited per API key. The limits are sized for production traffic; you
should never hit them under normal use, but build retry-on-429 into any high-volume
integration.

---

## Limits

There is **no per-minute cap on payments**. Charges, captures, payment-intent confirms and
refunds are not counted against a payment budget: a promotion that puts many buyers on the
checkout in the same minute is never refused for volume. Card testing is stopped by fraud
screening (velocity on IP, card and email, before the issuer is asked), not by counting
your payments.

### Global volume ceiling

**Every** endpoint is subject to a per-principal volume limit across three rolling windows:

| Window | Default limit |
|---|---|
| 1 second | 200 requests |
| 1 minute | 5,000 requests |
| 1 hour | 100,000 requests |

The principal is your API key (falling back to the client IP for unauthenticated calls).
These ceilings are configurable per merchant. Contact support if your steady-state
volume needs more.

These paths are exempt from the global ceiling, so health probes and the docs surface are
never throttled:

```
/health   /healthz   /readyz   /v1/docs   /v1/openapi.json   /v1/ping
```

---

## When you exceed a limit

You get a **429** with the standard envelope:

```json
{
  "error": {
    "type": "rate_limit_error",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded (200/1s)"
  }
}
```

Every 429 carries a **`Retry-After`** header (seconds): the window that was hit (`1`,
`60`, or `3600`), or `60` for a 429 that did not set its own value. There are no `RateLimit-*` headers. The `message` names the
limit that was hit.

---

## Handling 429 well

Honour `Retry-After`. It names the window that was hit (`1`, `60` or `3600` seconds), so a
retry sooner than that will 429 again:

```js
async function withRetry(fn, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    const res = await fn();
    if (res.status !== 429) return res;
    const wait = Number(res.headers.get("Retry-After") ?? 60) * 1000;
    await new Promise(r => setTimeout(r, wait));
  }
  throw new Error("rate limited after retries");
}
```

For `POST /charges`, pair the retry with an [`Idempotency-Key`](./12-idempotency.md) so a
retried charge replays the original result instead of charging twice.

If your steady-state volume needs higher limits, contact support. The per-key ceilings
are configurable.
