Core concepts
Rate limits
Budgets are per key, per minute, and set by the plan the key belongs to. Every response tells you where you stand, including the ones that fail.
Per-minute budget by plan
The budget is counted per key rather than per account, so separate keys for separate integrations do not compete for one window. An unknown or missing plan falls back to the most restrictive real tier: an unrecognised plan must not be a way to get a larger budget than any plan grants.
Headers
Three on every response, plus a fourth when you are refused. They are present on errors too, including a 401, which is the response a client is most likely to meet while being set up.
X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-AfterHTTP/1.1 200 OKX-RateLimit-Limit: 120X-RateLimit-Remaining: 117X-RateLimit-Reset: 1786454400Illustrative values for a Growth key.
Being refused
A refusal is a 429 carrying the wait in two places: the Retry-After header for transports that understand it, and error.retry_after for clients that only read the body.
HTTP/1.1 429 Too Many RequestsRetry-After: 14X-RateLimit-Limit: 120X-RateLimit-Remaining: 0 { "error": { "code": "rate_limited", "message": "Rate limit exceeded for this key", "retry_after": 14 }}Backing off
Wait the interval the response names. Exponential backoff is the fallback for a missing header, not the first move: guessing longer than the server asked wastes your own throughput.
async function call(url, headers, attempt = 0) { const res = await fetch(url, { headers }); if (res.status !== 429) return res; // The response says how long to wait. Prefer it over a guess, and only // fall back to exponential backoff if the header is missing. const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt; if (attempt >= 4) throw new Error("rate_limited: giving up after 5 attempts"); await new Promise((r) => setTimeout(r, wait * 1000)); return call(url, headers, attempt + 1);}Live keys fail closed, test keys fail open
The two kinds of key behave differently when the limiter itself cannot be read, and the asymmetry is deliberate rather than an oversight.
livekeys reach real data and real cost. A quota counted per instance is not a quota, because the number of warm instances rises exactly when you are under load. An unreadable limiter is therefore a429with a short retry rather than a guess.testkeys return simulated rows, touch no customer record and cost nothing. Refusing them when the limiter blinks would break somebody’s integration tests to protect nothing, so they are allowed through.
Reduce calls before raising limits
/v1/sync tells you when a platform last delivered new data, so you can poll that cheaply and only re-read metrics when the freshness date has actually moved.