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:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 583
X-RateLimit-Reset: 27
Retry-After: 27 # only on 429
Handling 429
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.