Rate Limits
Per-IP and per-endpoint request limits, the headers that expose them, and how to retry safely
3 min readThe API enforces two layers of rate limiting: a global per-IP budget that protects every route,
and tighter windows on auth-critical endpoints like sign-in and two-factor verification. Both
layers return 429 Too Many Requests with a stable error code and standard headers you can read
on every response.
Global limit
A single IP may issue up to 500 requests per 15 minutes across all routes combined.
| Window | Max requests | Scope |
|---|---|---|
| 15 minutes | 500 | Per IP, global |
Per-endpoint auth limits
On top of the global budget, auth-critical endpoints have stricter windows to prevent credential stuffing and brute-force.
| Endpoint | Max attempts | Window |
|---|---|---|
/api/auth/sign-in/email | 5 | 60 seconds |
/api/auth/sign-up/email | 3 | 60 seconds |
/api/auth/two-factor/* | 5 | 60 seconds |
/api/auth/passkey/* | 10 | 60 seconds |
/api/auth/magic-link/* | 5 | 1 hour |
Hitting any of these returns 429 with code RATE_LIMIT_ERROR. Wait for the window to expire — there is no retry-with-backoff strategy that beats the limit.
Response headers
Every response carries standard rate-limit headers.
| Header | Description |
|---|---|
RateLimit-Limit | Maximum requests permitted in the current window |
RateLimit-Remaining | Requests remaining in the current window |
RateLimit-Reset | Seconds until the window resets |
Retry-After | Present on 429 responses — seconds to wait before retrying |
HTTP/1.1 200 OK
RateLimit-Limit: 500
RateLimit-Remaining: 482
RateLimit-Reset: 612
Content-Type: application/json; charset=UTF-8
{
"success": true,
"status": 200,
"code": "OK",
"message": "Projects fetched successfully",
"data": [...]
}429 response
When you exceed the global limit, the API returns:
{
"success": false,
"status": 429,
"code": "RATE_LIMIT_ERROR",
"message": "Too many requests from this IP, please try again later.",
"meta": {
"message": "Too many requests from this IP, please try again later."
}
}The accompanying Retry-After header tells you how many seconds to wait. Honour it.
Retry with backoff
Use exponential backoff for 429 and transient 5xx responses. Read Retry-After first; fall back to 2^attempt seconds if the header is missing.
async function requestWithBackoff(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429 && res.status < 500) return res;
if (attempt === maxRetries) return res;
const retryAfter = Number(res.headers.get('Retry-After')) || Math.pow(2, attempt);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
}import time, requests
def request_with_backoff(method, url, max_retries=3, **kwargs):
for attempt in range(max_retries + 1):
res = requests.request(method, url, **kwargs)
if res.status_code != 429 and res.status_code < 500:
return res
if attempt == max_retries:
return res
retry_after = int(res.headers.get('Retry-After', pow(2, attempt)))
time.sleep(retry_after)Staying under the limit
- Cache reads: Cache responses whose underlying data changes slowly (reference data, organization settings, user profile).
- Batch writes when possible: Use endpoints that accept arrays rather than calling a single-resource endpoint in a loop.
- Poll sparingly: For long-running work, poll every few seconds rather than every few milliseconds.
- Use webhooks when available: Subscribing to provider webhooks is far cheaper than polling for state changes.
- Observe your budget: Log
RateLimit-Remainingon hot paths; a low floor is a signal to back off preemptively.
Need higher limits?
If your integration legitimately needs more headroom, contact your account manager with details about your use case and traffic pattern.
Best Practices
- Read
Retry-Afterbefore guessing a delay: The server knows exactly when the window resets. Your backoff math is an approximation. - Never retry sign-in or 2FA on
429: These use credential-safety limits. Retrying only extends the lockout. - Log
code === 'RATE_LIMIT_ERROR'separately: Distinguish rate limits from generic4xxso you can spot noisy callers. - Distribute traffic across IPs where legitimate: The limiter is per-IP. A single bot from one IP will hit the cap long before a fleet of workers across a NAT pool.
- Do not rotate IPs to dodge the limit: That is treated as abuse and can get the account suspended.
Next Steps
- Handle errors: Review Error Handling for the full list of codes including
RATE_LIMIT_ERROR. - Scope down: Confirm your Scopes & Permissions before you ship anything that runs in a tight loop.
- Plan for SSO: If you administer an organization, set up Enterprise SSO.
- Explore endpoints: Jump into the API Reference.
Related Pages
Introduction
How the API is structured, how authentication works, and how multi-tenant requests are scoped
Quick Start
Sign in, issue a Personal Access Token, and make your first authenticated call
Connect via MCP
Point any MCP-capable AI client at the hosted Snooplytics MCP server and drive the platform over OAuth — no token to paste.
API Structure
Request headers, response envelope, pagination, and the query conventions shared by every endpoint