Rate Limits
Understand API rate limiting, response headers, and retry strategies.
The ioZen API enforces rate limits per API key to ensure fair usage and platform stability. Limits use a sliding window algorithm — there's no fixed reset point, and your available quota replenishes gradually.
Limits by Plan
| Plan | Requests per Minute | Max API Keys |
|---|---|---|
| Free | — | 0 (no API access) |
| Pro | 100 | 3 |
| Business | 500 | 10 |
Rate limits apply per API key, not per workspace. If your workspace has multiple keys, each gets its own quota.
Rate Limit Headers
Every API response includes rate limit information in the headers:
| Header | Type | Description |
|---|---|---|
X-RateLimit-Limit | integer | Maximum requests allowed per window |
X-RateLimit-Remaining | integer | Requests remaining in the current window |
X-RateLimit-Reset | integer | Unix timestamp (milliseconds) when the window resets |
These headers are present on all responses, including error responses.
When Rate Limited
If you exceed your limit, the API returns a 429 Too Many Requests response:
{
"success": false,
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded",
"details": {
"retry_after": 12
}
}
}The retry_after value is in seconds — wait at least this long before retrying.
Retry Strategies
JavaScript — Exponential Backoff with Jitter
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) return response;
const body = await response.json();
const retryAfter = body.error?.details?.retry_after ?? 1;
const jitter = Math.random() * 1000;
const delay = retryAfter * 1000 + jitter;
await new Promise((resolve) => setTimeout(resolve, delay));
}
throw new Error('Max retries exceeded');
}Python — Exponential Backoff with Jitter
import time
import random
import requests
def fetch_with_retry(url, headers, max_retries=3):
for attempt in range(max_retries + 1):
response = requests.get(url, headers=headers)
if response.status_code != 429:
return response
body = response.json()
retry_after = body.get("error", {}).get("details", {}).get("retry_after", 1)
jitter = random.uniform(0, 1)
time.sleep(retry_after + jitter)
raise Exception("Max retries exceeded")Best Practices
- Check headers proactively. Monitor
X-RateLimit-Remainingand slow down before hitting the limit — don't wait for a429. - Use webhooks instead of polling. Instead of polling
GET /submissionsevery few seconds, register a webhook forsubmission.completedevents. - Cache responses where possible to reduce API calls. Intake bot configurations change infrequently.
- Batch work during off-peak hours. If you have bulk operations, spread them across time to stay within limits.
- Use a single API key per integration. This gives each integration its own rate limit quota and makes monitoring easier.
Timeout Behavior
If the rate limit check itself takes longer than 3 seconds (e.g., due to Redis latency), the API allows the request through rather than blocking it. This fail-open behavior ensures that temporary infrastructure issues don't prevent legitimate API usage.