Rate limits and errors
Read the quota headers on every API response, handle 429s correctly, and understand the error format and the codes you are most likely to meet.
The public API limits how many requests you can make, and tells you how much you have left on every response. When something goes wrong, every error has the same shape, so your code can switch on one field.
Request quotas
Requests are counted in fixed windows per minute and per day, both per API key and per organisation. The organisation's limit is the ceiling: all your keys together can never exceed it, whatever a single key's own limit says.
The limits depend on your organisation's settings. Read them from the headers rather than hard-coding them. Most requests count as one; a media upload (POST /media) counts as five.
Headers on every response
| Header | Meaning |
|---|---|
RateLimit-Limit | The quota of the window closest to running out |
RateLimit-Remaining | Requests left in that window |
RateLimit-Reset | Seconds until that window resets (a number of seconds, not a timestamp) |
RateLimit-Policy | Every window that applies, for example "key-minute";q=120;w=60, "key-day";q=100000;w=86400, … |
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | The same numbers, under the names many client libraries look for |
In RateLimit-Policy, q is the quota and w is the window length in seconds.
Two different 429s
code | What it means | What to do |
|---|---|---|
quota_exceeded | Your quota for a window is used up. details.scope says whether it was the key's (key) or the organisation's (tenant). | Wait for the number of seconds in Retry-After, which is when the window resets. |
rate_limited | You are sending bursts faster than the platform accepts. Retry-After is about 1 second. This answer has no RateLimit-* headers. | Pause briefly and retry. Spread your requests out. |
A media URL import can also give 429 rate_limited with details.reason: media_import_capacity when your organisation already has 2 imports running.
Tip: Watch RateLimit-Remaining and slow down before it reaches zero. You can see your usage over time in API usage.
Plan allowances (402)
Separately from these short windows, your plan may cap API requests or messages sent per billing period. When such an allowance is used up, requests are refused with 402 usage_limit_reached. details says which meter (api_requests or messages_sent), how much is used, the allowance and the period_end. Retrying does not help. A plan change does. See Usage.
The error format
Every error response has this body, never nested under an error key:
{
"code": "validation_failed",
"message": "A human-readable explanation.",
"request_id": "01J9…",
"details": {
"fields": [{ "field": "text.body", "message": "…" }]
}
}code: switch on this. It is a fixed list and won't change meaning.message: for people. It may change between releases, so don't parse it.request_id: the same as theX-Request-Idresponse header. Quote it when you contact support.details: extra context for some codes, such as the fields that failed validation or the permission that was missing.
Status codes
| Status | Common code values | Meaning |
|---|---|---|
200 / 201 / 202 | Success. 202 on a send means accepted for delivery, not yet delivered. | |
400 | validation_failed, media_too_large, unsupported_message_type | Something in the request is wrong. details.fields names each problem. |
401 | unauthenticated | No valid API key. See Authenticate API requests. |
402 | plan_feature_unavailable, plan_limit_reached, subscription_restricted, usage_limit_reached, wallet_balance_low | Your plan, subscription or WhatsApp credit balance stops this. No key or permission can lift it. |
403 | permission_denied | The key lacks the permission this endpoint needs. details.permission names it. |
404 | not_found | No such item in your organisation. Items of other organisations give the same answer. |
409 | window_expired, template_required, template_not_approved, contact_opted_out, duplicate_request, media_unavailable, conflict | The request is valid but the current state refuses it. |
413 | request_too_large | The request body is too large. |
429 | quota_exceeded, rate_limited | Too many requests. See above. |
503 | unavailable | The service could not answer right now. Retry. |
Billing-related 402 codes in more detail
code | Meaning | Useful details |
|---|---|---|
plan_feature_unavailable | Your plan does not include this feature. | feature, plan, upgrade_plan |
plan_limit_reached | A count limit of your plan is reached. | entitlement, limit, used |
subscription_restricted | Your subscription is restricted or cancelled, so template sends are paused. Replies inside the 24-hour window still work. | status, action |
usage_limit_reached | A plan allowance is used up for the billing period. | meter, used, allowance, period_end |
wallet_balance_low | Your WhatsApp credit balance cannot cover a template message of this category. Replies inside the 24-hour window are never refused for this. | balance_minor, required_minor, wallet_category, top_up_path |
See Plans and trial and WhatsApp credits.
Good habits
- Retry safely. On a timeout or a
503, retry with backoff. ForPOST /messages, reuse the sameIdempotency-Keyso the customer never gets a message twice. - Don't retry
4xxanswers unchanged, except429after waitingRetry-After. - Log
request_idwith every error.
If a message was accepted but never arrived, see Messages not delivered.
Still need help?
Write to us at [email protected] and tell us what you were trying to do. Already a customer? Sign in to IconEngage — the same articles are under Help in the app.