> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usezentra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Rate Limits

> Understanding API rate limits and quotas

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:

| Plan           | Rate Limit             | Burst Limit            |
| -------------- | ---------------------- | ---------------------- |
| **Sandbox**    | 100 requests/minute    | 150 requests/minute    |
| **Starter**    | 1,000 requests/minute  | 1,500 requests/minute  |
| **Growth**     | 5,000 requests/minute  | 7,500 requests/minute  |
| **Scale**      | 10,000 requests/minute | 15,000 requests/minute |
| **Enterprise** | Custom                 | Custom                 |

<Info>
  **Burst Limit** allows short spikes above your base rate limit for up to 60 seconds.
</Info>

## 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

| Header                  | Description                          |
| ----------------------- | ------------------------------------ |
| `X-RateLimit-Limit`     | Total requests allowed per window    |
| `X-RateLimit-Remaining` | Remaining requests in current window |
| `X-RateLimit-Reset`     | Unix timestamp when the limit resets |

## Rate Limit Exceeded

When you exceed the rate limit, you'll receive a `429 Too Many Requests` response:

```json theme={null}
{
  "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:

<CodeGroup>
  ```javascript JavaScript theme={null}
  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)}`);
  }
  ```

  ```python Python theme={null}
  import requests
  from datetime import datetime

  response = requests.get(
      'https://api.usezentra.com/api/v1/transfers',
      headers={'Authorization': f'Bearer {api_key}'}
  )

  remaining = int(response.headers.get('X-RateLimit-Remaining', 0))
  reset = int(response.headers.get('X-RateLimit-Reset', 0))

  if remaining < 10:
      print(f'Only {remaining} requests remaining')
      print(f'Resets at {datetime.fromtimestamp(reset)}')
  ```

  ```php PHP theme={null}
  $response = $client->get('https://api.usezentra.com/api/v1/transfers');

  $remaining = $response->getHeader('X-RateLimit-Remaining')[0];
  $reset = $response->getHeader('X-RateLimit-Reset')[0];

  if ($remaining < 10) {
      echo "Only {$remaining} requests remaining\n";
      echo "Resets at " . date('Y-m-d H:i:s', $reset);
  }
  ```
</CodeGroup>

### Implement Backoff

Use exponential backoff when rate limited:

```javascript theme={null}
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:

<CodeGroup>
  ```javascript JavaScript theme={null}
  const client = new Zentra.Client({
    apiKey: 'your_key',
    retryConfig: {
      maxRetries: 3,
      backoffMultiplier: 2
    }
  });

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

  ```python Python theme={null}
  client = Client(
      api_key='your_key',
      retry_config={
          'max_retries': 3,
          'backoff_multiplier': 2
      }
  )

  transfers = client.transfers.list()
  ```

  ```php PHP theme={null}
  $client = new Client([
      'api_key' => 'your_key',
      'retry_config' => [
          'max_retries' => 3,
          'backoff_multiplier' => 2
      ]
  ]);

  $transfers = $client->transfers->list();
  ```
</CodeGroup>

## Best Practices

<AccordionGroup>
  <Accordion title="Cache Responses">
    Cache API responses when appropriate to reduce redundant requests.

    ```javascript theme={null}
    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;
    }
    ```
  </Accordion>

  <Accordion title="Use Webhooks">
    Instead of polling for updates, use [webhooks](/api-reference/webhooks/overview) for real-time notifications.

    ```javascript theme={null}
    // 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);
      }
    });
    ```
  </Accordion>

  <Accordion title="Implement Request Queues">
    Use a queue to control the rate of requests.

    ```javascript theme={null}
    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);
      }
    }
    ```
  </Accordion>
</AccordionGroup>

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

| Endpoint                                                                | Operational Note                                                                                           |
| ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `POST /api/v1/identity/customers/{customer_id}/kyc/{check_type}/verify` | Identity verification routes can enforce tighter per-customer throttles when the tenant feature is enabled |
| `POST /api/v1/cards/{card_id}/fund`                                     | Funding 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

<CardGroup cols={2}>
  <Card title="Upgrade Plan" icon="arrow-up" href="https://www.usezentra.com/contact">
    View available plans
  </Card>

  <Card title="Contact Sales" icon="envelope" href="/support/contact">
    Discuss custom limits
  </Card>
</CardGroup>

## Testing Rate Limits

In sandbox, test your `429` handling against a harmless reviewed list route and honor the returned `Retry-After` header:

```bash theme={null}
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](https://status.usezentra.com)

## Next Steps

<CardGroup cols={2}>
  <Card title="Error Handling" icon="triangle-exclamation" href="/api-reference/errors">
    Handle rate limit errors properly
  </Card>

  <Card title="Webhooks" icon="webhook" href="/api-reference/webhooks/overview">
    Use webhooks instead of polling
  </Card>

  <Card title="Optimization Guide" icon="gauge-high" href="/guides/going-live">
    Optimize for production
  </Card>

  <Card title="Contact Support" icon="headset" href="/support/contact">
    Questions about limits?
  </Card>
</CardGroup>
