Errors
Bird returns a single error envelope on every failed request.
The error envelope
{
"type": "rate_limited",
"message": "Too many requests. Retry after 1.0s.",
"doc_url": "https://bird.dev/docs/errors#rate_limited",
"request_id": "req_3nB91x..."
}
type is drawn from a closed union — new types are added; existing types are never renamed. Branch on type, never on message. Quote request_id in any support request.
HTTP status code mapping
| Code | Meaning |
|---|---|
400 | Malformed request — missing required field, bad JSON, unknown parameter. |
401 | Authentication failed — key missing, malformed, or revoked. |
403 | Authenticated but not authorized — scope, sender, region, or missing User-Agent. |
404 | Resource not found, or your key can't see it. |
409 | State conflict — usually an idempotency key reused with a different payload. |
422 | Validation failed — shape is correct, values are not. |
429 | Rate limited — back off and honor Retry-After. |
5xx | Bird's problem. Retry with backoff; we're already paged. |
Error type registry
type | HTTP | Meaning | What to do |
|---|---|---|---|
invalid_request | 400 | Body is malformed JSON or contains unknown parameters. | Fix the request shape. |
invalid_recipient | 422 | Recipient format is wrong for the channel. | Use RFC-5322 for email, E.164 for SMS, voice, WhatsApp. |
missing_required_field | 400 | A required field is absent. | The param field on the envelope names the missing field. |
authentication_failed | 401 | Key missing, malformed, or revoked. | Check the prefix matches the environment and the key is current. |
permission_denied | 403 | Key lacks the required scope, sender, or IP allowlist entry. | Issue a key with the scope named in param, or update allowlist. |
not_found | 404 | Resource ID doesn't resolve under this key. | Confirm the ID prefix and that the key has read access. |
conflict | 409 | Resource is in a state that disallows this transition. | Inspect the resource's current state. |
idempotency_key_in_use | 409 | Same Idempotency-Key is in flight on another request. | Retry after a moment; the in-flight request will complete. |
rate_limited | 429 | Over the per-team rate. | Honor Retry-After (seconds). Safe to retry. |
quota_exceeded | 429 | Account-level monthly cap reached. | Contact support to lift the cap. |
template_not_approved | 422 | WhatsApp template hasn't been approved by Meta. | Wait for approval, or fix and resubmit. |
session_window_expired | 422 | WhatsApp free-form send outside the 24-hour session window. | Send an approved template instead. |
sender_not_verified | 403 | from isn't a verified sender on this account. | Verify the domain or number in the dashboard. |
carrier_rejected | 424 | Carrier rejected the send for content or recipient reasons. | Check carrier_trace. Sometimes retryable. |
recipient_unsubscribed | 422 | Recipient is on your suppression list. | Do not send. This is the system working. |
internal_error | 500 | Unexpected failure on our side. | Retry with backoff. We're already paged. |
service_unavailable | 503 | Upstream dependency (carrier, Meta) is degraded. | Retry with backoff. |
Retry guidance
Safe to retry with exponential backoff:
rate_limited— honorRetry-Afterfirstinternal_errorservice_unavailablecarrier_rejected(sometimes — inspectcarrier_tracefirst)
Do not retry — the request will fail the same way:
invalid_request,invalid_recipient,missing_required_fieldpermission_denied,authentication_failedrecipient_unsubscribed,sender_not_verifiedtemplate_not_approved,session_window_expired
Pair retries with an Idempotency-Key so duplicates don't double-send.
Idempotency keys + retries
Every POST accepts an Idempotency-Key header. Replays within 24 hours return the original response with Idempotency-Replayed: true, so retrying after a network blip is safe. Reusing the same key with a different payload returns 409 idempotency_key_in_use and the conflicting_fields array tells you which fields differed.