Skip to main content

Errors

Errors use conventional status codes with a machine-readable detail: 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 return 429 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 returns 503 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:
Pass 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.