# Rate Limits

> Per-tier request limits, the rate limit response headers, and how to handle 429 responses with exponential backoff.
> Source: https://developers.coinbax.com/docs/api/rate-limits
> Last updated: 2026-07-16

Coinbax APIs enforce tier-based rate limits per credential. Limits exist to
keep the platform predictable for every integration; well-behaved clients
that respect the response headers never hit them.

## Tiers

| Tier | Limit | Applies to |
|---|---|---|
| Public | 20 req/min | Unauthenticated endpoints |
| Standard | 100 req/min | Regular authenticated API calls |
| High-frequency | 1000 req/min | Session polling and other frequent-access endpoints |
| Admin | 50 req/min | Administrative operations (deliberately strict) |

Endpoints are assigned to tiers server-side; you do not select a tier. Most
of your traffic falls under Standard.

## Response headers

Every response tells you where you stand:

```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1735073400
```

| Header | Meaning |
|---|---|
| `X-RateLimit-Limit` | Requests allowed in the current window |
| `X-RateLimit-Remaining` | Requests left in the window |
| `X-RateLimit-Reset` | Unix timestamp when the window resets |
| `Retry-After` | Seconds to wait; sent on 429 responses only |

## When you exceed a limit

The API returns 429 with the standard envelope
(`error.code: "RATE_LIMIT_EXCEEDED"`) and a `Retry-After` header:

```
HTTP/1.1 429 Too Many Requests
Retry-After: 42
```

Two rules:

1. **Honor `Retry-After` when present.** It is the authoritative wait time.
2. **Fall back to exponential backoff with jitter** when it is absent or
   when retrying network-level failures.

## Backoff implementation

```javascript
async function coinbaxWithRetry(path, init, maxRetries = 5) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(`${BASE}${path}`, init);

    if (res.status !== 429) return res;
    if (attempt >= maxRetries) {
      throw new Error(`Rate limited after ${maxRetries} retries`);
    }

    const retryAfter = res.headers.get("Retry-After");
    const waitMs = retryAfter
      ? Number(retryAfter) * 1000
      // Exponential backoff with full jitter: 1s, 2s, 4s, 8s, 16s (capped)
      : Math.random() * Math.min(1000 * 2 ** attempt, 30_000);

    await new Promise((r) => setTimeout(r, waitMs));
  }
}
```

The jitter matters: if a burst of workers all hit the limit together, fixed
backoff makes them retry together and hit it again. Randomized waits spread
the retries out.

## Staying under the limits

- **Prefer webhooks to polling.** One [webhook subscription](/docs/api/webhooks)
  replaces a polling loop entirely; you receive every state transition as
  it happens instead of spending your budget asking.
- **Watch `X-RateLimit-Remaining`.** Treat a low value as a signal to slow
  down before you hit 429, not after.
- **Batch where the API supports it.** `POST /transactions/batch` creates
  multi-recipient payments in one request.
- **Cache stable reads.** Template configuration and platform settings
  change rarely; do not re-fetch them per transaction.
- **Use one credential per integration.** Limits apply per credential, so
  separate integrations do not consume each other's budget, and a noisy
  deploy cannot starve an unrelated system.

## Next steps

- [Errors and the response envelope](/docs/api/errors): the 429 response body in context
- [Webhooks](/docs/api/webhooks): event delivery instead of polling