Per-IP and per-endpoint request limits, the headers that expose them, and how to retry safely
4 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.
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 |
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 |
These endpoints are throttled by the auth layer, which responds differently from the rest of the
API: the body is a bare {"message": "Too many requests. Please try again later."} — no
envelope and no code field — and the wait time arrives in an X-Retry-After header
(seconds), not Retry-After. Wait for the window to expire — there is no retry-with-backoff
strategy that beats the limit.
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 global-limit 429 responses — seconds to wait before retrying |
X-Retry-After | Present on 429s from the /api/auth/* endpoints listed above — same meaning |
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 responseWhen 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.
When a per-endpoint auth limit trips instead, the response is not the envelope:
HTTP/1.1 429 Too Many Requests
X-Retry-After: 42
Content-Type: application/json
{"message": "Too many requests. Please try again later."}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') ?? res.headers.get('X-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') or res.headers.get('X-Retry-After') or pow(2, attempt))
time.sleep(retry_after)RateLimit-Remaining on hot paths; a low floor is a signal to back off preemptively.If your integration legitimately needs more headroom, contact your account manager with details about your use case and traffic pattern.
Retry-After before guessing a delay: The server knows exactly when the window resets. Your backoff math is an approximation.429: These use credential-safety limits. Retrying only extends the lockout.code === 'RATE_LIMIT_ERROR' separately: Distinguish rate limits from generic 4xx so you can spot noisy callers.RATE_LIMIT_ERROR.How the API is structured, how authentication works, and how multi-tenant requests are scoped
Sign in, issue a Personal Access Token, and make your first authenticated call
Point any MCP-capable AI client at the hosted MCP server and drive Indexhog over OAuth — no token to paste.
Request headers, response envelope, pagination, and the query conventions shared by every endpoint