Skip to main content

Rate Limits

To ensure the stability and performance of the Zentra API, we enforce rate limits on all API requests.

Rate Limit Tiers

Rate limits vary based on your plan:
PlanRate LimitBurst Limit
Sandbox100 requests/minute150 requests/minute
Starter1,000 requests/minute1,500 requests/minute
Growth5,000 requests/minute7,500 requests/minute
Scale10,000 requests/minute15,000 requests/minute
EnterpriseCustomCustom
Burst Limit allows short spikes above your base rate limit for up to 60 seconds.

Rate Limit Headers

Every API response includes rate limit information in the headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1704711600

Header Descriptions

HeaderDescription
X-RateLimit-LimitTotal requests allowed per window
X-RateLimit-RemainingRemaining requests in current window
X-RateLimit-ResetUnix timestamp when the limit resets

Rate Limit Exceeded

When you exceed the rate limit, you’ll receive a 429 Too Many Requests response:
{
  "success": false,
  "error": {
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded. Retry after 60 seconds",
    "status": 429,
    "details": {
      "limit": 1000,
      "window": "1 minute",
      "retry_after": 60
    }
  }
}
Additionally, the response includes a Retry-After header:
Retry-After: 60

Handling Rate Limits

Check Headers

Always check rate limit headers before making additional requests:
const response = await fetch('https://api.usezentra.com/api/v1/transfers', {
  headers: {
    'Authorization': `Bearer ${apiKey}`
  }
});

const remaining = response.headers.get('X-RateLimit-Remaining');
const reset = response.headers.get('X-RateLimit-Reset');

if (remaining < 10) {
  console.warn(`Only ${remaining} requests remaining`);
  console.log(`Resets at ${new Date(reset * 1000)}`);
}

Implement Backoff

Use exponential backoff when rate limited:
async function makeRequestWithBackoff(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error.status === 429) {
        const retryAfter = error.details?.retry_after || Math.pow(2, i);
        console.log(`Rate limited. Waiting ${retryAfter}s...`);
        await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

const transfers = await makeRequestWithBackoff(() =>
  client.transfers.list()
);

SDK Support

Our official SDKs handle rate limiting automatically:
const client = new Zentra.Client({
  apiKey: 'your_key',
  retryConfig: {
    maxRetries: 3,
    backoffMultiplier: 2
  }
});

const transfers = await client.transfers.list();

Best Practices

Cache API responses when appropriate to reduce redundant requests.
const cache = new Map();

async function getCachedTransferStatus(transferId) {
  if (cache.has(transferId)) {
    return cache.get(transferId);
  }

  const transfer = await client.transfers.get(transferId);
  cache.set(transferId, transfer);
  return transfer;
}
Instead of polling for updates, use webhooks for real-time notifications.
// Bad: Polling every 5 seconds
setInterval(async () => {
  const status = await client.transfers.get(transferId);
}, 5000);

// Good: Use webhooks
app.post('/webhooks/zentra', (req, res) => {
  if (req.body.event === 'transfer.completed') {
    handleTransferComplete(req.body.data);
  }
});
Use a queue to control the rate of requests.
class RequestQueue {
  constructor(requestsPerMinute) {
    this.queue = [];
    this.interval = 60000 / requestsPerMinute;
    this.processing = false;
  }

  async add(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing || this.queue.length === 0) return;

    this.processing = true;
    const { fn, resolve, reject } = this.queue.shift();

    try {
      const result = await fn();
      resolve(result);
    } catch (error) {
      reject(error);
    }

    setTimeout(() => {
      this.processing = false;
      this.process();
    }, this.interval);
  }
}

Endpoint-Specific Safeguards

Some reviewed public routes may have stricter operational safeguards than the base per-minute limit, depending on tenant configuration and product controls:
EndpointOperational Note
POST /api/v1/identity/customers/{customer_id}/kyc/{check_type}/verifyIdentity verification routes can enforce tighter per-customer throttles when the tenant feature is enabled
POST /api/v1/cards/{card_id}/fundFunding routes can carry stricter replay and risk controls than simple list operations
GET /api/v1/webhooks/cards/events/{card_id}Use pagination and caching for event review instead of hot-loop polling

Increasing Limits

Need higher rate limits?
  1. Upgrade Your Plan: Higher plans come with increased limits
  2. Contact Sales: Enterprise customers can get custom limits
  3. Optimize Usage: Follow best practices to reduce API calls

Upgrade Plan

View available plans

Contact Sales

Discuss custom limits

Testing Rate Limits

In sandbox, test your 429 handling against a harmless reviewed list route and honor the returned Retry-After header:
curl "https://sandbox.api.usezentra.com/api/v1/transfers?page=1&limit=1" \
  -H "Authorization: Bearer sk_sandbox_your_key"
Zentra does not currently expose a dedicated reviewed public test/rate-limit route.

Status Page

Check real-time API performance and any rate limiting issues: status.usezentra.com

Next Steps

Error Handling

Handle rate limit errors properly

Webhooks

Use webhooks instead of polling

Optimization Guide

Optimize for production

Contact Support

Questions about limits?