Understand Zenovay API rate limits and optimize your integration for reliability and performance.
Rate Limit Overview
The REST API is a paid feature. API keys only work on Pro, Scale, and Enterprise plans — Free-plan keys are rejected with a 403 API_PAID_PLAN_REQUIRED response.
API Limits by Plan
These limits apply to the REST API (endpoints under /api/external/v1/):
| Plan | Requests/Minute | Requests/Month |
|---|---|---|
| Pro | 30 | 10,000 |
| Scale | 60 | 100,000 |
| Enterprise | 120 | 1,000,000 |
Limits are resolved from your team's subscription. If you belong to multiple teams, the API uses the highest plan you're a member of.
What Counts as a Request
Each authenticated API call counts as one request against your monthly quota — the read endpoints (GET) and the query endpoint (POST /query/:websiteId). The REST API is read-only; there are no PUT, PATCH, or DELETE endpoints.
What Doesn't Count
Requests that fail authentication (401, e.g. a missing or invalid key) don't count toward your quota, since the key is never validated.
Rate Limit Headers
Response Headers
Every API response includes usage headers:
X-RateLimit-Limit: 30
X-Usage-Monthly: 4521
X-Usage-Limit: 10000
X-Usage-Reset: 2026-07-01T00:00:00.000Z
X-Usage-Total: 89234
| Header | Description |
|---|---|
| X-RateLimit-Limit | Your per-minute request limit |
| X-RateLimit-Remaining | Requests left this minute (not always present — see below) |
| X-Usage-Monthly | Requests used this month |
| X-Usage-Limit | Monthly request limit |
| X-Usage-Reset | When monthly usage resets (ISO 8601) |
| X-Usage-Total | Total requests ever made with this key |
X-RateLimit-Remaining is only included when an exact remaining count is available; treat its absence as normal and rely on X-RateLimit-Limit plus your own request count for budgeting.
Reading Headers
const response = await fetch(url, { headers });
const limit = response.headers.get('X-RateLimit-Limit');
const remaining = response.headers.get('X-RateLimit-Remaining'); // may be null — see note above
const monthlyReset = response.headers.get('X-Usage-Reset'); // ISO 8601 timestamp
console.log(`${remaining ?? '?'}/${limit} requests remaining this minute`);
console.log(`Monthly usage resets at ${monthlyReset}`);
Rate Limit Exceeded
Response
When the per-minute limit is exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 42
{
"success": false,
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded (30 requests/minute). Try again in 42 seconds",
"timestamp": "2026-06-13T12:00:00.000Z"
}
}
The per-minute 429 includes a Retry-After header (in seconds) telling you how long to wait before retrying.
When the monthly quota is exceeded, the error body uses the same shape with "code": "MONTHLY_LIMIT_EXCEEDED". This response does not carry a Retry-After header — check the X-Usage-Reset header instead, which tells you when your monthly allowance resets (the first of the next month).
Handling 429 Errors
async function apiRequest(url, options, retries = 3) {
const response = await fetch(url, options);
if (response.status === 429 && retries > 0) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`Rate limited. Waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
return apiRequest(url, options, retries - 1);
}
return response;
}
Best Practices
Caching
Cache responses when possible:
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getWithCache(endpoint) {
const cached = cache.get(endpoint);
if (cached && Date.now() - cached.time < CACHE_TTL) {
return cached.data;
}
const response = await api.get(endpoint);
cache.set(endpoint, { data: response, time: Date.now() });
return response;
}
Request Only What You Need
Use the range filter to limit the data returned:
# Filter to a specific time range:
GET /api/external/v1/analytics/WEBSITE_ID?range=7d
Use Date Filters
Limit data returned to specific periods (supported values: 24h, 7d, 30d, 90d, 1y):
# Fetch only the last 30 days:
GET /api/external/v1/analytics/WEBSITE_ID?range=30d
Pagination
The visitors endpoint paginates with limit and offset (not page numbers). limit defaults to 100 and is capped at 1000.
Request
GET /api/external/v1/analytics/WEBSITE_ID/visitors?range=30d&limit=100&offset=0
Response
{
"success": true,
"data": {
"visitors": [...],
"pagination": {
"limit": 100,
"offset": 0,
"has_more": true
}
},
"timestamp": "2026-06-13T12:00:00.000Z"
}
Efficient Pagination
Keep requesting pages until has_more is false, advancing offset by limit each time:
async function getAllVisitors(websiteId) {
let allVisitors = [];
let offset = 0;
const limit = 100;
let hasMore = true;
while (hasMore) {
const response = await api.get(
`/analytics/${websiteId}/visitors?limit=${limit}&offset=${offset}`
);
const { visitors, pagination } = response.data;
allVisitors = allVisitors.concat(visitors);
hasMore = pagination.has_more;
offset += limit;
// Respect rate limits
await sleep(100);
}
return allVisitors;
}
Exponential Backoff
Implementation
async function requestWithBackoff(fn, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (error.status === 429 && attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000 + Math.random() * 1000;
console.log(`Retry ${attempt + 1} after ${delay}ms`);
await sleep(delay);
} else {
throw error;
}
}
}
}
Backoff Schedule
| Attempt | Wait Time |
|---|---|
| 1 | 1-2 seconds |
| 2 | 2-4 seconds |
| 3 | 4-8 seconds |
| 4 | 8-16 seconds |
| 5 | 16-32 seconds |
Monitoring Usage
Check Current Usage
curl https://api.zenovay.com/api/external/v1/usage \
-H "X-API-Key: zv_YOUR_API_KEY"
This returns your current monthly request count, remaining quota, per-minute rate limit, and subscription tier.
Every API response also carries your live usage in headers (X-Usage-Monthly, X-Usage-Limit, X-Usage-Reset), so you can track consumption without a separate call.
In the dashboard
Open API Keys (app.zenovay.com/api-keys) to see your keys and each key's request totals and recent activity. Use the GET /usage endpoint above for the precise monthly count and remaining quota.
Optimizing High-Volume Use
Use Aggregated Endpoints
Use the analytics overview endpoint to get summary totals in a single call instead of paging through raw visitor records:
# Single request for analytics overview:
GET /api/external/v1/analytics/WEBSITE_ID?range=30d
This returns a summary block (total visitors, page views, unique visitors) plus daily_stats in one response. For geographic, page, or device breakdowns, use the dedicated /countries, /pages, and /technology endpoints.
Enterprise Rate Limits
Enterprise PlanEnterprise plans start at the highest published limits (120 requests/minute, 1,000,000 requests/month). If you need higher limits, email [email protected] with:
- Your current usage patterns
- Expected growth
- Your use case details
Troubleshooting
Frequently Rate Limited
If you hit the per-minute limit often:
- Add delays between requests (or a request queue)
- Use exponential backoff on 429s
- Cache responses you reuse
- Use aggregated endpoints instead of many small calls
- Upgrade your plan for a higher limit
Monthly Quota Exhausted
A MONTHLY_LIMIT_EXCEEDED (429) means you've used your monthly request allowance. It resets on the first of the next month (see the X-Usage-Reset header). Upgrade your plan for a higher monthly quota.
Unexpected Low Limits
If your limits seem wrong:
- Verify your plan level — limits are based on your team's subscription
- Confirm you're using a key from the right team
- Contact support