# Rate limits & errors

Four enforcement levels, and how to stay under them.

Limits are enforced hierarchically — burst, then installation, then app, then platform. Test and live traffic are counted separately, so sandbox load can never eat live quota.

## Per-installation quotas

| Group | Test | Live |
| --- | --- | --- |
| Read | 120 req/min, burst 10/s | 600 req/min, burst 30/s |
| Write | 60 req/min, burst 5/s | 300 req/min, burst 15/s |
| Heavy (analytics, exports) | 20 req/min, burst 2/s | 60 req/min, burst 5/s |

## App-wide ceilings

Across all installations of one app: **3 000 req/min** in test, **30 000 req/min** in live. A generous platform ceiling sits above that as an emergency brake.

## Headers

Every response carries:

```text
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 583
X-RateLimit-Reset: 27
Retry-After: 27      # only on 429
```

## Handling 429

```js
async function call(url, token, attempt = 0) {
  const res = await fetch(url, { headers: { Authorization: `Bearer ${token}` } });
  if (res.status === 429 && attempt < 5) {
    const wait = Number(res.headers.get("Retry-After") ?? 1) * 1000;
    await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
    return call(url, token, attempt + 1);
  }
  return res;
}
```

Add jitter, cap retries, and never retry a `403`.

## Staying efficient

- React to webhooks instead of polling.
- Cache `/v1/merchant`; it changes rarely.
- Use time-window filters for backfills, and run them off-peak.
- When usage crosses 80 % of an app quota we notify you before throttling bites.
