Catalogian enforces rate limits per API key to ensure fair usage. This page covers limits by plan, how to detect rate limiting, and how to handle errors.
| Endpoint | Limit | Window |
|---|---|---|
| REST API (authenticated) | 600 requests | Per minute, per API key |
| REST API (unauthenticated) | 100 requests | Per minute, per IP |
| MCP endpoint | 30 requests | Per minute, per API key |
| OpenAI Responses endpoint | 30 requests | Per minute, per API key |
MCP calls also count toward the daily MCP limit on the Free plan (50 calls/day). Paid plans have unlimited daily MCP calls.
Every API response includes rate limit headers:
X-RateLimit-Limit: 600 X-RateLimit-Remaining: 594 X-RateLimit-Reset: 1710849600
| Header | Description |
|---|---|
X-RateLimit-Limit | Max requests allowed in the current window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
When you exceed the rate limit, the API returns a 429 status:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json
{
"error": "Rate limit exceeded. Try again in 12 seconds."
}Use the Retry-After header to determine how long to wait before retrying. Implement exponential backoff for robust retry logic:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) return res;
const retryAfter = parseInt(res.headers.get("Retry-After") || "5");
await new Promise(r => setTimeout(r, retryAfter * 1000));
}
throw new Error("Rate limit exceeded after retries");
}| Status | Meaning | Common causes |
|---|---|---|
| 400 | Bad Request | Invalid JSON body, missing required field, invalid query parameter |
| 401 | Unauthorized | Missing or invalid API key |
| 402 | Payment Required | Plan limit reached (sources, SKUs, or features) |
| 403 | Forbidden | API key lacks the required scope for this endpoint |
| 404 | Not Found | Source, delta event, or resource doesn't exist (or belongs to another account) |
| 429 | Too Many Requests | Rate limit exceeded — see Retry-After header |
| 500 | Internal Server Error | Unexpected server error — contact support if persistent |
All errors return a JSON body with a single error field:
{
"error": "Human-readable error message"
}Error messages are safe to display to end users — they never contain internal details, stack traces, or other accounts' data.
Retry-After headerLearn about cursor-based pagination. Pagination →