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/* or /v2/* endpoint counts against a per-project request limit. Requests to both versions share one window. 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 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.
How to Increase Your Limit
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.
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.
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:
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);
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{ "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 does.
TypeScriptimport 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; } }
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.
CodeLimit: 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 | { 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, and the balance cannot pay for more (see pay as you go) | { error: { code, message, details } } |
Best Practices
Check X-RateLimit-Remaining to avoid hitting the limit unexpectedly.
Wait the seconds in the Retry-After header, or until details.retryAfter, before retrying.
For bulk sending, use the Broadcasts API instead of individual requests.
Include idempotencyKey in requests to safely retry without duplicates.
Bulk Sending Example
For sending many messages from your own loop, pace on the headers:
TypeScriptimport 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; }
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 |