Errors
Errors use conventional status codes with a machine-readabledetail:
Structured errors always carry a stable
code you can branch on; extra keys (like conflicting_contact_id on a duplicate contact) give you what you need to resolve it. Every error shape is published in the API reference.
Sending a template
POST /v1/templates/{id}/send refuses the whole request rather than queuing part of it, so a rejection never leaves you half-sent and never costs you anything. The codes worth branching on:
GET /v1/templates/{id}/send-requirements tells you what a template needs before you send, so most of these never have to happen.
Rate limits
Each organization has a shared budget across all its keys, set by its plan: 20 requests per second on Scale, 10 per second on Growth, and 5 per second during the free trial. Over the budget, requests return429 with a Retry-After header (seconds; always 1, since the window is one second).
Every response, including the 429, carries your current budget, so you can pace without counting calls yourself:
Read
X-RateLimit-Remaining and slow down as it approaches zero rather than sprinting into the 429. If your integration genuinely needs more, talk to us.
Requests running at the same time
The API also limits how many requests one key can have running at once. Past that limit, or when the API as a whole is at capacity, a request returns503 with "server_busy" and Retry-After: 1. The request did not run and does not count against your budget, so retry it after the wait.
Calls that wait for each other’s response won’t see it; a burst of many parallel calls can. To send a template to many people, put up to 100 recipients in each POST /v1/templates/{id}/send instead of making one call per recipient.
One request is one request no matter how much it does.
POST /v1/templates/{id}/send counts once whether it carries one recipient or a hundred, so batching recipients into a single send is the cheapest way to stay inside the budget. What bounds actual message volume is WhatsApp’s own rolling 24-hour limit, not this one.Retrying safely
POST /v1/templates/{id}/send accepts an Idempotency-Key header. Send the same key again within 24 hours and you get the original broadcast back instead of a second send. Use this after a timeout, when you cannot tell whether the request landed. Use a fresh key for each distinct send.
Pagination
List endpoints return:next_cursor back as ?cursor= to fetch the next page; a null cursor means you’ve reached the end. Cursors are opaque, so don’t parse them.
Property and funnel-stage lists aren’t paginated: they return your full set, with no next_cursor.
Compatibility
This is an early API and it changes often. Two habits keep your integration stable:- Most changes are additive: new endpoints, new optional parameters, new response fields, new values in string fields. Tolerate unknown fields and values, and ignore what you don’t recognize.
- Every contract change is listed in the changelog. Check it when something looks different.