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

# Rate Limiting

> Per-project request limits on the Zavu API: the 600 requests per minute default, how it rises with verification, 429 responses, Retry-After, and how it differs from sending limits

## Rate Limiting

Every authenticated request to a `/v1/*` endpoint counts against a per-project request limit. It protects the platform from bursts; it is not a sending quota. The daily ceilings per channel, the per-recipient SMS cooldown and the monthly allowances are separate rules with separate error codes. They are covered in [Sending limits](/concepts/sending-limits) and summarized at the end of this page.

## Request Limits

| Limit | Who gets it | How it is set |
| - | - | - |
| **600 requests/min** | Every project | The default |
| **1,200 requests/min** | Every project in a team whose Business Verification (KYB) is approved | Applied automatically when the verification is approved |
| **2,400 requests/min** | Every project in a team whose 10DLC brand has been verified by the registry | Applied automatically when the brand is verified |

A limit that Zavu set for your account by hand is never lowered by these automatic steps, and neither is a limit that is already higher.

<Info>
  Counting is per project: each project has its own window and its own headers. The tier itself is set for every project in the team at once.
</Info>

### How to Increase Your Limit

<Steps>
  <Step title="Complete Business Verification (KYB)">
    In the dashboard, go to **Compliance** and open **Business verification**. When it is approved, every project in the team moves to **1,200 requests/min**.
  </Step>

  <Step title="Register a 10DLC brand (US SMS)">
    Register your brand for 10DLC. When the registry verifies it, every project in the team moves to **2,400 requests/min**. The campaign is not what raises the request limit; it is what lets you send SMS to US numbers.
  </Step>
</Steps>

<Tip>
  The increase is applied the moment the verification is approved. Nothing changes in your code: the new limit shows up in `X-RateLimit-Limit` on your next request.
</Tip>

## Rate Limit Headers

Every `/v1/*` response includes headers to help you track your usage:

| Header | Description | Example |
| - | - | - |
| `X-RateLimit-Limit` | Maximum requests per minute for this project | `600` |
| `X-RateLimit-Remaining` | Requests left in the current window | `542` |
| `X-RateLimit-Reset` | Unix timestamp (ms) when the current window ends | `1701234600000` |
| `Retry-After` | Seconds to wait (only on 429) | `45` |

## Checking Rate Limits with the SDK

Call `.withResponse()` on the request to get the parsed result together with the raw HTTP response:

<CodeGroup>
  ```typescript TypeScript theme={null}
  import Zavudev from '@zavudev/sdk';

  const zavu = new Zavudev({
    apiKey: process.env['ZAVUDEV_API_KEY'],
  });

  const { data, response } = await zavu.messages
    .send({ to: "+14155551234", text: "Hello from Zavu!" })
    .withResponse();

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

  console.log(`Rate limit: ${remaining}/${limit} requests remaining`);
  console.log(`Window ends at: ${new Date(Number(reset)).toISOString()}`);

  console.log("Message ID:", data.message.id);
  ```

  ```bash cURL theme={null}
  curl -i -X POST https://api.zavu.dev/v1/messages \
    -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"to": "+14155551234", "text": "Hello!"}'

  # Response headers will include:
  # X-RateLimit-Limit: 600
  # X-RateLimit-Remaining: 599
  # X-RateLimit-Reset: 1701234600000
  ```
</CodeGroup>

## Rate Limit Exceeded (429)

When a project exceeds its limit, the API returns `429 Too Many Requests` with a `Retry-After` header (in seconds) and this body:

```json theme={null}
{
  "code": "rate_limited",
  "message": "Rate limit exceeded. Retry after 2024-01-15T12:30:00.000Z",
  "details": {
    "retryAfter": 1705322400000
  }
}
```

`details.retryAfter` is the same instant as `X-RateLimit-Reset`, in Unix milliseconds.

If the rate limiter itself cannot be reached, requests are refused with this same response and a 60-second `Retry-After` rather than let through unchecked.

## Handling Rate Limits

The TypeScript SDK retries a 429 on its own, up to 2 times by default, waiting for the `Retry-After` the API sent. Set `maxRetries` higher for bursty traffic; handle the error yourself only once the retries are exhausted. For the other SDKs, check the status on the API error the same way [the SDK overview](/sdks/overview#error-handling) does.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import Zavudev from "@zavudev/sdk";

  const zavu = new Zavudev({
    apiKey: process.env["ZAVUDEV_API_KEY"],
    maxRetries: 4,
  });

  async function sendWithRateLimitHandling(to: string, text: string) {
    try {
      const { data, response } = await zavu.messages
        .send({ to, text })
        .withResponse();

      const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
      if (remaining < 10) {
        console.warn(`Low rate limit: ${remaining} requests remaining`);
      }

      return data;
    } catch (error) {
      if (error instanceof Zavudev.RateLimitError) {
        // Retries are exhausted. Retry-After is in seconds.
        const waitMs = Number(error.headers.get("retry-after") ?? 60) * 1000;
        console.log(`Rate limited. Waiting ${waitMs}ms before retry...`);
        await new Promise((resolve) => setTimeout(resolve, waitMs));
        return sendWithRateLimitHandling(to, text);
      }
      throw error;
    }
  }
  ```

  ```python Python theme={null}
  import os
  import time
  from zavudev import Zavudev, APIError

  zavu = Zavudev(
      api_key=os.environ.get("ZAVUDEV_API_KEY"),
  )

  def send_with_rate_limit_handling(to: str, text: str):
      try:
          return zavu.messages.send(to=to, text=text)
      except APIError as e:
          if e.status_code == 429:
              print("Rate limited. Waiting 60s before retry...")
              time.sleep(60)
              return send_with_rate_limit_handling(to, text)
          raise
  ```

  ```ruby Ruby theme={null}
  require "zavudev"

  client = Zavudev::Client.new(
    api_key: ENV["ZAVUDEV_API_KEY"]
  )

  def send_with_rate_limit_handling(client, to:, text:)
    client.messages.send_(to: to, text: text)
  rescue Zavudev::Errors::APIError => e
    raise unless e.status == 429
    puts "Rate limited. Waiting 60s before retry..."
    sleep(60)
    send_with_rate_limit_handling(client, to: to, text: text)
  end
  ```

  ```go Go theme={null}
  func sendWithRateLimitHandling(client *zavudev.Client, to, text string) (*zavudev.MessageResponse, error) {
      result, err := client.Messages.Send(context.TODO(), zavudev.MessageSendParams{
          To:   zavudev.String(to),
          Text: zavudev.String(text),
      })
      if err != nil {
          var apiErr *zavudev.APIError
          if errors.As(err, &apiErr) && apiErr.StatusCode == 429 {
              fmt.Println("Rate limited. Waiting 60s before retry...")
              time.Sleep(60 * time.Second)
              return sendWithRateLimitHandling(client, to, text)
          }
          return nil, err
      }
      return result, nil
  }
  ```

  ```php PHP theme={null}
  use Zavudev\APIError;

  function sendWithRateLimitHandling($client, string $to, string $text) {
      try {
          return $client->messages->send(['to' => $to, 'text' => $text]);
      } catch (APIError $e) {
          if ($e->status === 429) {
              echo "Rate limited. Waiting 60s before retry...\n";
              sleep(60);
              return sendWithRateLimitHandling($client, $to, $text);
          }
          throw $e;
      }
  }
  ```
</CodeGroup>

## How It Works

The limit is a **sliding window** over the last 60 seconds: at any moment, a project may have at most its limit of requests in the previous minute. There is no wall-clock reset. Capacity comes back as older requests age out of the window, so a burst that hits the limit at 12:00:30 is not cleared at 12:01:00; it clears as the requests from 12:00:30 onward fall out of the window.

```
Limit: 600 in any 60-second window
├── 12:00:00  request 1     OK (remaining: 599)
├── 12:00:20  request 600   OK (remaining: 0)
├── 12:00:21  request 601   429 rate_limited
└── 12:01:00  request 1 has aged out: one slot is back
```

`X-RateLimit-Remaining` tells you how many requests fit right now; waiting a full 60 seconds always empties the window.

## Other 429 responses that are not rate limits

`POST /v1/messages` and `POST /v1/calls` also answer 429 for limits on what you send, not on how fast you call the API. They are different rules with different codes, and none of them is raised by the request limit above.

| Code | What it means | Body shape |
| - | - | - |
| `daily_limit_exceeded` | The channel group's daily ceiling for your account level is spent. See [Sending limits](/concepts/sending-limits) | `{ code, message, details: { limit, remaining, dashboardUrl } }` |
| `rate_limit_exceeded` | One SMS per recipient per minute: a second `sms` or `sms_oneway` message to the same number within 60 seconds. The message says how many seconds to wait | `{ error: { code, message } }` |
| `a2p_limit_exceeded` | The Free plan's monthly allowance of 2,000 messages across WhatsApp, Telegram, Instagram and Messenger is spent | `{ error: { code, message, details } }` |

## Best Practices

<CardGroup cols={2}>
  <Card title="Monitor Headers" icon="gauge">
    Check `X-RateLimit-Remaining` to avoid hitting the limit unexpectedly.
  </Card>

  <Card title="Honor Retry-After" icon="clock">
    Wait the seconds in the `Retry-After` header, or until `details.retryAfter`, before retrying.
  </Card>

  <Card title="Use Broadcasts" icon="tower-broadcast">
    For bulk sending, use the [Broadcasts API](/guides/broadcasts/overview) instead of individual requests.
  </Card>

  <Card title="Use Idempotency Keys" icon="key">
    Include `idempotencyKey` in requests to safely retry without duplicates.
  </Card>
</CardGroup>

## Bulk Sending Example

For sending many messages from your own loop, pace on the headers:

```typescript theme={null}
import Zavudev from '@zavudev/sdk';

const zavu = new Zavudev({
  apiKey: process.env['ZAVUDEV_API_KEY'],
});

async function sendBulkMessages(messages: { to: string; text: string }[]) {
  const results = [];
  let remaining = 600;

  for (const msg of messages) {
    // If running low, wait for the window to slide
    if (remaining < 10) {
      console.log("Approaching rate limit, waiting 60 seconds...");
      await new Promise(resolve => setTimeout(resolve, 60000));
      remaining = 600;
    }

    const { data, response } = await zavu.messages.send(msg).withResponse();

    remaining = Number(response.headers.get("X-RateLimit-Remaining"));
    results.push(data);
  }

  return results;
}
```

<Warning>
  For sending to more than 100 recipients, use the [Broadcasts API](/guides/broadcasts/overview) instead. Broadcasts send in paced batches and report progress. Each recipient still counts against your daily sending ceiling.
</Warning>

## Summary

| Limit | How to get it |
| - | - |
| 600/min | Every project, by default |
| 1,200/min | Business Verification (KYB) approved |
| 2,400/min | 10DLC brand verified by the registry |

## Next Steps

<CardGroup cols={2}>
  <Card title="Sending Limits" icon="shield-check" href="/concepts/sending-limits">
    Daily ceilings and destination rules per account level
  </Card>

  <Card title="Broadcasts API" icon="tower-broadcast" href="/guides/broadcasts/overview">
    Send to many recipients efficiently
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.