WhatsApp campaigns
This is the module that adds the most to an integration: launching a campaign through the API means inheriting the send pacing, per-recipient tracking, resumption after interruption and delivery counter recalculation. On top of a plain send endpoint, all of that has to be rewritten, and it breaks on the first batch of a thousand numbers.
List
GET /whatsapp/campaigns?status=active&limit=20
Track a campaign
GET /whatsapp/campaigns/{id}
Returns the counters: sent, delivered, read, failed.
GET /whatsapp/campaigns/{id}/recipients?status=failed
Recipient-by-recipient detail, with the failure reason where there is one. That is what lets you retry precisely instead of resending everything.
Control
| Call | Effect |
|---|---|
POST /whatsapp/campaigns/{id}/launch | Starts sending |
POST /whatsapp/campaigns/{id}/pause | Suspends without losing progress |
POST /whatsapp/campaigns/{id}/resume | Resumes where it stopped |
POST /whatsapp/campaigns/{id}/cancel | Stops for good |
POST /whatsapp/campaigns/{id}/retry | Resends only to failed recipients |
Launching returns as soon as the queue is built: it does not wait for the send to finish, which continues server-side. Track progress with GET /whatsapp/campaigns/{id}, or receive it on your webhook.
Add recipients
POST /whatsapp/campaigns/{id}/recipients
{ "phones": ["+2250700000000", "+2250700000001"] }
Also accepts customerIds to target customers already stored in the shop.
What the campaign rules give you
- Controlled pacing: sends are spread out to stay within Meta's rate limits. A hand-written send loop degrades the number's quality rating, then gets it blocked.
- Resumption: an interruption does not lose progress, and already-processed recipients are not sent to again.
- Per-recipient tracking: every recipient carries its own status, not just a global counter.
- Consent: contacts who opted out are excluded automatically.
Campaign statuses
draft, scheduled, active, paused, completed, cancelled.
A campaign belongs to one store
Calls carrying a campaign identifier check that it belongs to your key's store. An identifier from another store returns resource_not_found, exactly like a non-existent one: the response gives away nothing about what exists.