Rate limits
A rate limit is the number of requests you may make in a period of time. SanadWA limits requests per API key, per minute.
The limit
Section titled “The limit”- The default is 60 requests per minute for each API key.
- An admin can set a different limit for a key, from 1 to 3600 requests per minute. Set it when you create the key (Rate limit (req/min)), or later with Edit in Settings → Developers → API keys.
- Every request with a valid key counts, including
GETrequests and requests that end in an error. - Requests rejected because the key is missing, wrong, blocked by the IP allowlist or lacks the scope are rejected before the limit is checked.
Creating campaigns: 5 per minute
Section titled “Creating campaigns: 5 per minute”POST /campaigns has a second, lower limit: 5 requests per minute for each API key. It counts
separately from the general limit, and every request to POST /campaigns counts toward both,
including retries that return an earlier answer. Above it you get 429 rate_limited with
Retry-After, and this message:
{ "success": false, "code": "rate_limited", "message": "This action is limited to 5 requests per minute for each API key. Retry in 40s."}This limit cannot be changed per key. It exists so that a script stuck in a loop cannot start many campaigns.
WhatsApp’s own limits
Section titled “WhatsApp’s own limits”This limit is separate from WhatsApp’s own limits on how many messages a number may send.
If WhatsApp refuses a message because of its limits, you get a send_failed error,
not rate_limited.
Rate-limit headers
Section titled “Rate-limit headers”Responses to requests with a valid key include these headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Your limit, in requests per minute. |
X-RateLimit-Remaining |
How many more requests you can make before you are limited. |
HTTP/1.1 200 OKX-RateLimit-Limit: 60X-RateLimit-Remaining: 47These headers describe the general limit only, not the campaign limit.
Use X-RateLimit-Remaining to slow down before you reach the limit.
There is no header that says when the limit resets.
When you go over the limit
Section titled “When you go over the limit”You get HTTP status 429 and this body:
HTTP/1.1 429 Too Many RequestsRetry-After: 12X-RateLimit-Limit: 60X-RateLimit-Remaining: 0
{ "success": false, "code": "rate_limited", "message": "Rate limit exceeded for this API key. Retry in 12s."}Retry-After is the number of seconds to wait before the next request. It is at least 1.
Do not count on the allowance coming back bit by bit during the minute. When you reach the limit,
wait the full Retry-After time before you send more requests.
A 429 means the request was not processed. No message was sent. It is safe to send the same
request again after the wait, with the same idempotency key.
How to back off
Section titled “How to back off”- When you get
429, readRetry-Afterand wait that many seconds. - Then retry the same request.
- If you get
429again, wait longer each time (for example double the wait), and add a small random delay so that many workers do not retry at the same moment. - To avoid
429in the first place, spread your sends over time. A queue in your system that sends at most your limit per minute works well.
async function callWithBackoff(doRequest, maxAttempts = 5) { for (let attempt = 1; ; attempt++) { const res = await doRequest(); if (res.status !== 429 || attempt === maxAttempts) return res; const retryAfter = Number(res.headers.get("Retry-After")) || 1; const wait = retryAfter * 1000 * 2 ** (attempt - 1) + Math.random() * 500; await new Promise((r) => setTimeout(r, wait)); }}import randomimport time
def call_with_backoff(do_request, max_attempts=5): for attempt in range(1, max_attempts + 1): res = do_request() if res.status_code != 429 or attempt == max_attempts: return res retry_after = int(res.headers.get("Retry-After", "1")) time.sleep(retry_after * 2 ** (attempt - 1) + random.random() / 2)<?php// $doRequest returns [int $status, array $headers, string $body]function callWithBackoff(callable $doRequest, int $maxAttempts = 5): array { for ($attempt = 1; ; $attempt++) { [$status, $headers, $body] = $doRequest(); if ($status !== 429 || $attempt === $maxAttempts) return [$status, $headers, $body]; $retryAfter = max(1, (int)($headers["retry-after"] ?? 1)); usleep(($retryAfter * 2 ** ($attempt - 1)) * 1_000_000 + random_int(0, 500_000)); }}# curl can retry 429 responses and respects Retry-After:curl --retry 5 https://api.sanadwa.com/v1/templates \ -H "Authorization: Bearer $SANADWA_API_KEY"If your normal traffic needs more than 60 requests per minute, ask an admin of your account to raise the limit of that key.