Rate limits
60 requests per minute, per store, on a sliding one-minute window.
Headers
Every reply carries the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | 60 |
X-RateLimit-Remaining | Calls left in the current window |
Retry-After | On 429 only: seconds to wait |
Reading X-RateLimit-Remaining as you go is cheaper than being refused and backing off.
When you go over
{ "success": false, "data": null, "error": "Rate limit exceeded" }Status 429. Wait for Retry-After and retry — every endpoint is a GET, so a retry is always safe.
if (response.status === 429) {
const wait = Number(response.headers.get('Retry-After') || 60);
await new Promise(r => setTimeout(r, wait * 1000));
}The limit is per store, not per token
The bucket you are metered against is keyed by the store the token resolves to. Rotating a token does not give you a fresh allowance, and an integration serving many merchants gets 60/minute for each of them independently.
There is a second bucket in front of it, and it matters in exactly one case: calls are throttled before the token is checked, keyed by the token presented — or by your IP when there is no token at all. So a client hammering with a token that is wrong or revoked is refused with 429 rather than 401, and fixing the credential is the way out, not waiting.
What it is, and is not
This is a first-line guard against runaway callers, not a metered quota. It is enforced per server instance, so under load the effective ceiling can be higher than 60. Do not design around exceeding it — build to 60/minute and you will never meet it.
There is no monthly quota, no per-endpoint limit, and no billing attached to request volume.
Staying under it
Page instead of hammering. /data/offers returns 20 per page. Walking 200 offers is 10 calls, not 200.
Ask for the roll-up, not the parts. /data/analytics/summary is one call for shop-wide figures. Fetching each placement from /data/analytics/stats and adding them is both more calls and — because the columns do not share a denominator — the wrong answer.
Cache what does not move. /data/analytics/lifetime is all-time; polling it every minute tells you nothing new. Yesterday's closed date range will not change either.
Spread scheduled jobs. A nightly export that fires 60 calls in one second is refused halfway; the same 60 spread over a minute is not.