Rate limits
Every Bird API endpoint is rate limited. Limits exist to keep the platform stable for everyone; they are generous enough that well-behaved integrations rarely hit them, and every response tells you exactly where you stand so you never have to guess.
This page defines the shared rate-limit model and lists every group's base rate.
How limits are keyed
Rate limits are resolved per organization: every API key and workspace in the organization draws against the same resolved allowance. How that allowance is bucketed depends on the group:
- Send groups (email_send, sms_send, and the other per-product send and batch groups) share one organization-wide bucket. They are account quotas: it doesn't matter which key or workspace sends.
- Management groups (read, list, write) bucket per acting credential within the organization, so one runaway script can't drain the whole organization's read budget.
- Unauthenticated endpoints (login, signup, password reset) are keyed by client IP, with fixed abuse-prevention thresholds that are not adjustable.
Groups
Endpoints are grouped into rate limit groups, and the group sets the limit. Endpoints with similar costs share a group: read for single-resource reads, list for collection queries, and write for management changes. Expensive operations have dedicated groups such as email_send, email_batch, sms_send, whatsapp_send, and verify_send. When you hit a limit, only that group is exhausted. Running out of send quota does not block you from reading delivery status or managing webhooks.
The three management groups span every product and start at the same base rate for all organizations:
| Group | Base rate | Covers |
|---|---|---|
| list | 150/min | Collection queries: list messages, events, recipients, domains, suppressions. |
| read | 500/min | Single-resource lookups: a message by ID, a domain, a webhook, a suppression. |
| write | 60/min | Creates, updates, and deletes: domains, webhooks, suppressions. |
The management buckets count per acting credential within your organization, so one busy worker's polling can't starve another key's reads. Send buckets are org-wide account quotas, one per product, and every organization starts from the same one-minute-window base rates:
| Group | Base rate | Covers |
|---|---|---|
| email_send | 10/min | POST /v1/email/messages, plus mailbox sends (replies and forwards). |
| email_batch | 5/min | POST /v1/email/batches (one request queues up to 100 messages). |
| email_broadcast_send | 5/min | Creating a broadcast with immediate or scheduled send, and sending an existing broadcast draft. |
| contacts_batch | 30/min | POST /v1/contacts/batch (one request upserts up to 1,000 contacts). |
| sms_send | 10/min | POST /v1/sms/messages. |
| sms_batch | 5/min | POST /v1/sms/batches, up to 100 messages per request. |
| whatsapp_send | 10/min | POST /v1/whatsapp/messages. |
| verify_send | 10/min | POST /v1/verify/verifications (sending or resending a code) and /next-channel. |
| verify_check | 60/min | POST /v1/verify/verifications/check. |
Three things matter for the send buckets. They count requests, so batching increases throughput without consuming a bucket entry per recipient. At base rates, 10 individual email calls send 10 messages per minute, while 5 batch calls can queue 500. See the bulk-sending guides for email and SMS. whatsapp_send guards request volume, while your wallet controls per-message spend. The verify groups also limit account-level request volume. Separate abuse guardrails throttle verification requests and guesses for each recipient.
The group that matched your request is named in the rate-limit response headers, so you always know which bucket you exhausted.
One group has no endpoint behind it. voice_call limits outbound calls for an organization to a base rate of 5 per second. Bird enforces the limit when your phone system sets up a call, so no API response returns 429 or rate-limit headers. An exhausted bucket refuses the call and records calls_per_second_exceeded. See Rejected calls. The effective limit resolves through the same three layers as every other group.
Treat every number on this page as a starting point. Limits change: your plan raises them, a per-organization override replaces them (see How your limit is resolved), and base rates may be tuned over time. Your effective quota is always advertised on the response itself. Build clients that discover limits from live traffic instead of hardcoding the documented values.
How your limit is resolved
Your effective limit for a group is resolved from three layers:
- Base: the default that applies to every organization.
- Plan: your subscription plan raises the limit for specific groups.
- Override: a per-organization override, arranged with Bird, for customers whose volume outgrows their plan's ceiling. An active override replaces the base and plan values.
In practice the base is where every organization starts, your plan raises it, and an override is the escape hatch when you need more. If your traffic is approaching your plan's ceiling, contact support. Overrides routinely support higher-volume organizations. The RateLimit-Policy header advertises your current effective quota for a group on every response.
Response headers
Every response from a rate-limited endpoint carries two headers in the IETF Structured Fields format (RFC 9651):
Exemplo de código
RateLimit-Policy: "email_send";q=1000;w=60
RateLimit: "email_send";r=842;t=35| Header | Meaning |
|---|---|
| RateLimit-Policy | The policy that applies: q is the quota (max requests) and w is the window in seconds. |
| RateLimit | Your current state: r is the number of requests remaining and t is the seconds until the window resets. |
The quoted string names the group that matched the request. In this example, the org has an effective email_send limit of 1000 requests per 60 seconds, with 842 remaining and 35 seconds until reset.
Use r and t to slow requests before receiving a 429. Bird uses this format because t always means seconds from now, avoiding ambiguity with Unix timestamps. A single response can also express multiple policies when an endpoint is subject to more than one.
When you hit a limit
Every production integration must handle 429 responses as part of normal operation. At minimum, honor Retry-After and retry with backoff. A client that also paces itself against the live RateLimit headers (see Response headers) avoids reaching the limit.
An exhausted limit returns 429 Too Many Requests with a Retry-After header (in seconds, matching the t value) and both RateLimit headers, with r=0 and the name of the group you exhausted. The body is the standard error envelope; this one is a real response:
Exemplo de código
{
"error": {
"type": "rate_limit_error",
"code": "E01003",
"name": "RateLimited",
"message": "Too many requests. Please retry after the period indicated in the Retry-After header.",
"doc_url": "https://bird.com/docs/api/errors/E01003",
"request_id": "req_01ky7qavkff7qr88vadv6bv948"
}
}Branch on type: rate_limit_error. The human-readable message can change. Read the group name and retry timing from the headers:
async function sendWithBackoff(url, headers, payload, maxAttempts = 5) {
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(url, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
if (response.status !== 429) return response;
const retryAfter = Number(response.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000));
}
throw new Error("rate limited after max retries");
}import time
import requests
def send_with_backoff(url, headers, payload, max_attempts=5):
for attempt in range(max_attempts):
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 429:
return response
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
raise RuntimeError("rate limited after max retries")func sendWithBackoff(req *http.Request, maxAttempts int) (*http.Response, error) {
for attempt := range maxAttempts {
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusTooManyRequests {
return resp, nil
}
resp.Body.Close()
wait := 1 << attempt
if s, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = s
}
time.Sleep(time.Duration(wait) * time.Second)
}
return nil, errors.New("rate limited after max retries")
}function sendWithBackoff(ClientInterface $http, RequestInterface $request, int $maxAttempts = 5): ResponseInterface
{
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
$response = $http->sendRequest($request);
if ($response->getStatusCode() !== 429) {
return $response;
}
$retryAfter = (int) ($response->getHeaderLine('Retry-After') ?: 2 ** $attempt);
sleep($retryAfter);
}
throw new RuntimeException('rate limited after max retries');
}A rate-limited request is rejected before it does any work: it does not consume an idempotency key, so retrying with the same key is always safe.
The Bird SDKs handle all of this automatically: they detect 429s, honor Retry-After, and retry with backoff, so most SDK users never write this code.
Failure mode
The rate limiter fails open: if Bird cannot evaluate a limit, the request proceeds rather than receiving a spurious refusal. Rate limiting protects service capacity. Authentication and authorization remain the security boundaries. A Bird-side limiter outage does not cause a 429.
Next steps
- Errors: the error envelope and how to branch on error types
- Idempotency: safe retries for mutating requests
- SDK concepts: automatic retry and backoff behavior