View as Markdown

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

CallEffect
POST /whatsapp/campaigns/{id}/launchStarts sending
POST /whatsapp/campaigns/{id}/pauseSuspends without losing progress
POST /whatsapp/campaigns/{id}/resumeResumes where it stopped
POST /whatsapp/campaigns/{id}/cancelStops for good
POST /whatsapp/campaigns/{id}/retryResends 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.