API reference

Campaigns

Create, preview, send, pause and track a message to a whole audience.

Campaigns

A campaign sends one message to many of your opted-in contacts. The API gives you the same lifecycle the dashboard has: create a draft with its audience and content in one call, ask who it would actually reach, send now or schedule for later, watch recipients settle, pause or cancel mid-flight, and delete a campaign to free the plan slot it holds.

Everything here runs through the same code as the dashboard. Consent, suppression lists, plan limits and send pacing are enforced identically — there is no API path around them.

Scopes: campaigns:read, campaigns:write (draft lifecycle), and campaigns:send — schedule, send-now, pause, resume, cancel. Send is a separate grant because one call fires messages at an entire audience; see Authentication.

Create a campaign

POST /api/v1/campaigns
{
  "name": "July wholesale offer",
  "audience_definition": { "type": "list", "list_id": "3f8a…" },
  "content": { "body": "Our July rates are live — reply ORDER for the catalogue." }
}
Field Required Notes
name yes Internal name, up to 160 characters — recipients never see it
audience_definition no* Who receives it — see below. *Required before scheduling
content no* What they receive — see below. *Required before scheduling

Returns 201 with the campaign in draft status. Nothing is sent until you call schedule.

The audience

One of three shapes:

{ "type": "contacts", "contact_ids": ["d1c4…", "9e0b…"] }
{ "type": "list",     "list_id": "3f8a…" }
{ "type": "segment",  "segment_id": "77aa…" }

contacts takes 1–50,000 unique contact ids. Lists and segments are the ones in your dashboard. Whatever the shape, only opted-in, non-suppressed contacts are messaged — the rest are counted as skipped, never contacted.

The content

Your own words, a template, or words plus files:

{ "body": "Plain message" }
{ "template_id": "9b2f…" }
{ "body": "See attached", "media": { "attachments": [{ "media_id": "5c1d…" }] } }

Send either body or template_id, not both. A template is resolved to its words and its file at scheduling time — a later edit to the template cannot change what an approved campaign says. Per-recipient variables like {name} are filled at dispatch, per contact (see Templates).

Who would this reach?

POST /api/v1/campaigns/audience/preview
{ "audience_definition": { "type": "list", "list_id": "3f8a…" } }
{
  "data": { "matching": 184, "reachable": 171, "skipped": 13, "sample": ["Ayesha Khan", "Bilal Traders"] }
}

Nothing is saved. reachable is who will actually be messaged; skipped is who the audience covers but consent or suppression removes. The count is produced by the same query dispatch uses, and it can only go down between preview and send — anyone who opts out in the meantime is skipped automatically.

Send now, or schedule

POST /api/v1/campaigns/{id}/schedule

Send immediately:

{ "connection_id": "b7e2…", "send_now": true }

Or at a wall-clock time in your workspace's timezone (not the server's, not your machine's):

{ "connection_id": "b7e2…", "scheduled_at_local": "2026-08-01T09:00", "timezone": "Asia/Karachi" }
Field Notes
connection_id Which of your connected numbers sends. GET /workspace shows them
send_now true to start immediately
scheduled_at_local YYYY-MM-DDTHH:mm, must be in the future
timezone Must match the workspace's saved timezone — a mismatch is refused rather than silently reinterpreted

Both shapes validate the audience and content at this moment and refuse with 422 if the audience has no reachable recipient, the content is empty, or the plan's campaign allowance is exhausted. Requires campaigns:send.

Watch it run

GET /api/v1/campaigns/{id}
GET /api/v1/campaigns/{id}/recipients

The campaign carries live counters — recipient_count, sent_count, failed_count, status (draft, scheduled, running, paused, completed, cancelled, failed). Recipients are paginated, one row per person, each with a status and, on failure, a failure_code.

Prefer webhooks to polling: subscribe to campaign.started, campaign.completed and campaign.failed and the final counters come to you (see Webhook events).

How fast it sends

A campaign does not fire every message at once. These are session-based numbers, and a burst is exactly what spam detection looks for, so each number sends within its sending policy: a per-number hourly and daily cap and a small random gap between messages. A large audience is therefore spread out — released steadily and carried into the next hour or day when a cap is reached — rather than sent in one rush. The policy is conservative by default and fully editable in your workspace settings, per workspace and per number. This is why a big campaign shows running for a while: it is pacing itself on purpose to keep the number healthy. See Sending policy for what each control does.

The test batch before a big send

Before a campaign sends to everyone, it runs a small canary: it sends to a handful of recipients first, waits, and checks how that batch went — how many were delivered or failed, and the sending number's health. Only if the test batch looks safe does the rest of the campaign go out. If it looks risky, the campaign pauses itself instead of burning the number on the full send, and tells you why (for example, "the number's health is risky" or "60% of the test batch failed").

While this is happening the campaign shows it is checking a test batch; a canary that pauses the campaign leaves it in paused, exactly like a manual pause. Look at the number, then resume when you are ready — resuming is your go-ahead, so the campaign continues from where it stopped without re-running the test. A campaign.paused webhook fires with the reason.

The canary only runs when the audience is larger than the test batch (there is nothing to protect otherwise). Its size, how long it watches, how much failure it tolerates, and which health states stop it are all editable in Settings → Sending policy; it can be turned off entirely. The defaults are conservative.

The message risk check

As you write a campaign, its message is scored for the things WhatsApp's spam detection reacts to — several links, a link shortener, shouting in capitals, a phone number in the text, an over-long message — and the composer shows a small risk hint (amber for "watch", red for "higher risk") with the reasons. It is purely advisory: it never blocks a send, it just gives you the chance to soften the message first. What counts, and at what score, is editable in Settings → Sending policy.

Not messaging one person too often

Recipient safety is an optional guard, off until you turn it on in Settings → Sending policy. When on, it holds back a marketing send to a contact who has already been messaged within a window you set (so two campaigns in a day do not double up on the same people), and it holds back a link or media on the very first message to a brand-new contact — the riskiest send there is. A held recipient shows as skipped (with the reason) on the campaign's recipient list; it is not a failure, and the contact is untouched. Inbox replies and transactional API sends are never affected.

Pause, resume, cancel

POST /api/v1/campaigns/{id}/pause
POST /api/v1/campaigns/{id}/resume
POST /api/v1/campaigns/{id}/cancel

Pause stops the next batch, not the one already in flight. Resume picks up exactly where it left off — nobody is messaged twice. Cancel is permanent: recipients still waiting are marked cancelled, and what was already sent stays sent. All three require campaigns:send.

Update and delete

PUT /api/v1/campaigns/{id}
DELETE /api/v1/campaigns/{id}

Only a draft or scheduled campaign can be edited. Deleting frees the plan slot the campaign occupies — that is what to do when creation answers 422 with the allowance message. A campaign that is running cannot be deleted; cancel it first. Messages already sent are never recalled by any of this; only the record-keeping changes.

Was this guide useful? Report an issue