Skip to main content
Pro Plan5 minutesIntermediate

Rate Limits & Best Practices

Understand API rate limits - quotas, headers, and strategies for efficient API usage. Learn about api in this API integrations guide.

apirate-limitsperformancebest-practices
Last updated:
Pro Plan

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/):

PlanRequests/MinuteRequests/Month
Pro3010,000
Scale60100,000
Enterprise1201,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
HeaderDescription
X-RateLimit-LimitYour per-minute request limit
X-RateLimit-RemainingRequests left this minute (not always present — see below)
X-Usage-MonthlyRequests used this month
X-Usage-LimitMonthly request limit
X-Usage-ResetWhen monthly usage resets (ISO 8601)
X-Usage-TotalTotal 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

AttemptWait Time
11-2 seconds
22-4 seconds
34-8 seconds
48-16 seconds
516-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 Plan

Enterprise 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:

  1. Add delays between requests (or a request queue)
  2. Use exponential backoff on 429s
  3. Cache responses you reuse
  4. Use aggregated endpoints instead of many small calls
  5. 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:

  1. Verify your plan level — limits are based on your team's subscription
  2. Confirm you're using a key from the right team
  3. Contact support

Next Steps

Was this article helpful?