Rate limits
Per-key limits, rate limit headers, 429 handling with backoff, and how to request more headroom
Rate limits keep one integration's burst from degrading anyone else's — including yours. Limits apply per API key, so your server traffic, your CI suite, and each client app draw from separate budgets, and they exist in both test and live mode so you hit them in development, not production.
The limits
| Surface | Default limit |
|---|---|
Secret-key requests (sk_…) | 100 requests/second per key |
Publishable-key requests (pk_…) | 25 requests/second per key |
Event ingestion (POST /v1/events) | 500 requests/second per org, pooled across keys |
| Unauthenticated surfaces (link redirects, code lookup) | Per-IP limits, sized for real users rather than scripts |
Short bursts above the sustained rate are tolerated; sustained excess returns 429. Publishable keys get tighter limits than secret keys because they ship inside apps you do not control.
Event ingestion is special-cased. POST /v1/events is the hottest path in the product, so it has its own, much larger pool that is not consumed by (and does not consume) your other API traffic. A batch import that saturates event ingestion will not starve your dashboard or reward calls.
Rate limit headers
Every API response reports where you stand:
| Header | Meaning |
|---|---|
RateLimit-Limit | The request budget for the current window. |
RateLimit-Remaining | Requests left in the window. |
RateLimit-Reset | Seconds until the window resets. |
Retry-After | On 429 only — seconds to wait before retrying. |
Handling 429s
A 429 comes with the standard error envelope (type: "rate_limit_error", code: "rate_limit_exceeded") and a Retry-After header. Honor Retry-After, add jitter, and cap your attempts:
async function invitebaseFetch(
path: string,
init: RequestInit,
maxAttempts = 5,
): Promise<Response> {
for (let attempt = 1; ; attempt++) {
const res = await fetch(`https://api.invitebase.com${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.INVITEBASE_SECRET_KEY}`,
"Content-Type": "application/json",
...init.headers,
},
});
if (res.status !== 429) return res;
if (attempt >= maxAttempts) return res;
const retryAfter = Number(res.headers.get("Retry-After")) || 1;
const jitter = Math.random() * 500;
await new Promise((r) => setTimeout(r, retryAfter * 1000 + jitter));
}
}For POST requests, send the same Idempotency-Key on every attempt so a retry never double-creates — see Errors — retrying safely.
Beyond retries:
- Spread bulk work. Backfills and imports should run at a steady rate below the limit rather than as one burst; watch
RateLimit-Remainingand slow down as it approaches zero. - Queue writes on your side. If you fan events out from your own infrastructure, put a queue with concurrency control in front of the API instead of firing per-request.
- Don't poll — subscribe. If you are polling referral or reward state, switch to webhooks; the data comes to you and your read budget stays free.
The SDKs handle 429 backoff automatically; you only need this on direct API integrations.
Requesting higher limits
Limits are configurable per organization. If you are launching a campaign, migrating historical data, or consistently running near the default limits, contact support@invitebase.com with your expected request rates and which endpoints they hit — event-ingestion headroom in particular can be raised substantially. Enterprise plans include raised limits by default.