> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Run a WhatsApp marketing campaign end-to-end

> Take a WhatsApp campaign from approved template to launch to callbacks: bind the template on the campaign, pre-flight with the dry-run, launch, and wire the delivery, read, and reply webhooks.

# Run a WhatsApp marketing campaign end-to-end

This guide walks the full lifecycle of a business-initiated WhatsApp campaign: pick an approved template, draft the campaign with WhatsApp-specific fields, dry-run it, launch, and subscribe to the webhooks that report per-recipient outcomes. It complements the [campaign end-to-end guide](/guides/campaign-end-to-end), which covers the channel-agnostic sequence — everything here is the WhatsApp delta: template binding, Meta's gates, and the read/reply callbacks SMS never produces.

## 1. Prerequisites

* **A connected WABA with at least one approved `marketing` template.** Business-initiated WhatsApp sends leave only as templates — there is no free-form blast equivalent. If you have not onboarded yet, start with the [WABA setup guide](/guides/whatsapp/waba-setup); the [24-hour window guide](/guides/whatsapp/24h-window) explains why templates are the only permissible content outside an open customer-service window.
* **An API key with the `campaigns:write` scope.** Reads (audience preview, dry-run, stats) accept `campaigns:read`; launch requires write.
* **An opted-in recipient audience.** Opt-outs and suppression are enforced at send time; Meta additionally throttles marketing-template delivery for low-quality WABAs, so engagement-quality audiences matter more here than on SMS. Recipients outside the 24-hour window are reached by the template; recipients with an open window still count toward your per-recipient messaging-limit tier.

## 2. Pick and bind the approved template

The campaign references a template **by name**, and the launch pre-flights it against your locally synced template registry. A template whose Meta status is `PENDING`, `REJECTED`, `PAUSED`, or `DISABLED` fails the launch with a 422 — check the status under **Channels → WhatsApp → Templates** before you draft.

For a **single-channel WhatsApp campaign** (blast or scheduled), put the template name in the top-level `message_template` field. For an **omnichannel fallback chain**, bind the template to the WhatsApp chain entry instead — an SMS or email step has no template name, so the schema rejects `whatsapp_template` anywhere except a `channel: "whatsapp"` entry:

| Field                        | Notes                                                                                              |
| ---------------------------- | -------------------------------------------------------------------------------------------------- |
| `whatsapp_template.name`     | Approved template name from the Meta registry.                                                     |
| `whatsapp_template.language` | BCP-47 code (`en`, `en_US`, `pt_BR`, …). Optional — falls back to the template's default language. |
| `whatsapp_template.params`   | Ordered body-variable substitutions keyed by 1-based position string (`"1"`, `"2"`, …).            |

If your org's campaign metadata declares a `template_category_intent` (e.g. `MARKETING`), launch also verifies Meta's category on the template matches the intent — an `AUTHENTICATION` OTP template cannot ride a marketing blast.

## 3. Create the draft

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Summer sale — WhatsApp VIP drop",
    "type": "blast",
    "channel": "whatsapp",
    "audience_type": "list",
    "audience_id": "list_summerVIPs",
    "message_template": "summer_drop_vip_v2",
    "send_time_optimization": "fixed"
  }'
```

The response comes back in `draft` status; nothing sends until step 6. Per-recipient variable values (`{{1}}`, `{{2}}`, …) resolve from contact fields the same way `{{token}}` personalization does on other channels — preview them with the audience sample in step 4 before launch.

**Fallback chains.** To try WhatsApp first and fall back to SMS on terminal failure, pass a `channels` array whose first entry is WhatsApp carrying `whatsapp_template`, followed by an SMS entry:

```json theme={null}
"channels": [
  {
    "channel": "whatsapp",
    "order": 0,
    "fallback_on": "failed",
    "fallback_after_seconds": 300,
    "whatsapp_template": { "name": "summer_drop_vip_v2", "language": "en" }
  },
  { "channel": "sms", "order": 1 }
]
```

When a chain exists, the top-level `channel` must equal the chain's first entry, and `whatsapp_template` may only appear on the WhatsApp entry.

## 4. Preview the audience

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/audience/preview" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "audience_type": "list", "audience_id": "list_summerVIPs", "channel": "whatsapp" }'
```

Pass `channel: "whatsapp"` so the net projection subtracts suppression rows, WhatsApp opt-outs, and contacts with no WhatsApp-reachable number. A recipient whose only address is an email disappears from a WhatsApp preview — that cohort is your `unreachable` count.

## 5. Dry-run: the WhatsApp gates

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_abc123/dry-run" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

Beyond the standard buckets (net audience, cost, quiet hours, warnings), a WhatsApp campaign is gated on Meta-specific state:

* **Template pre-flight.** Launch resolves `message_template` against your synced template registry and requires `status: "approved"`. A missing template name means the local registry has never seen it — sync templates from Meta, then retry.
* **Quality rating.** If your WABA's quality rating has dropped to red, the campaign auto-pauses with reason `whatsapp_quality_red` rather than burning quota on sends Meta will throttle anyway. Clear the quality issue in **Channels → WhatsApp → Health**, then resume the campaign.
* **Messaging-limit tier.** The dry-run projects cost against the same resolver the send path uses, but the *audience size* must also fit your current per-24h unique-recipient tier (250 on a new WABA, scaling to unlimited). Check your tier under **Channels → WhatsApp → Health** before launching at a large list; an over-tier audience fails recipients at send time, not at launch.

## 6. Launch

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/campaigns/cmp_abc123/send" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

A 422 here with a template message points back at step 2 — re-read the template status and category. A 402 or a pending-approval shape is the standard balance/approvals gate, not WhatsApp-specific.

## 7. Callbacks: what WhatsApp reports that SMS cannot

Subscribe to both the campaign lifecycle events and the per-message events; the [webhook events catalog](/webhooks/events) lists every type your endpoint can subscribe to.

* **Campaign lifecycle** — `campaign.started`, `campaign.completed`, `campaign.paused` (including the `whatsapp_quality_red` auto-pause), `campaign.drip_step.sent` for drip shapes.
* **Per-message outcomes** — `message.sent` (accepted by the provider), `message.delivered` (device receipt), `message.read` (the recipient opened it — a signal SMS never produces), and `message.failed` carrying the Meta error code, e.g. `(#132015) too many business messages` when Meta throttles marketing deliveries.
* **Replies** — an inbound reply inside (or opening) the 24-hour window arrives as `message.received`. Route it into the inbox or your own automation; the [normalized inbound envelope](/webhooks/normalized-inbound-envelope) describes the payload shape, and the [webhook consumer guide](/guides/webhook-consumer) covers signature verification, retries, and dead letters.

`GET /api/v1/campaigns/:id/stats` aggregates the same signals — sent, delivered, read (`opened`), replied — so the dashboard view and your webhook-driven pipeline reconcile.

## 8. Read rate discipline

WhatsApp surface a read receipt SMS lacks; use it. If delivered stays high but read and replied trend toward zero, Meta's per-user marketing-frequency capping is already filtering your sends before recipients see them — shrink the audience toward engaged cohorts or move the same content into the 24-hour window as a free-form follow-up instead of another marketing template. Read the [24-hour window guide](/guides/whatsapp/24h-window) for the rules on when a reply opens that window, and the [pricing guide](/guides/whatsapp/pricing) for how marketing vs. utility template categories price each conversation.

## Troubleshooting

* **Launch 422: template not found locally.** The template registry has not synced the name — pull templates from Meta (Channels → WhatsApp → Templates → Sync) and retry. Re-check spelling; template names are case-sensitive.
* **Launch 422: template status is not approved.** Resubmit or pick an approved template. Only `APPROVED` templates can send; `PAUSED` and `DISABLED` count as blocked.
* **Campaign auto-paused with `whatsapp_quality_red`.** The WABA quality rating dropped to red. Resolve the quality issue, then `POST /api/v1/campaigns/:id/resume`.
* **High `message.failed` with Meta code `(#132015)`.** Meta is capping how many marketing-template messages one recipient receives in a window. This is recipient-side filtering, not an Orbit failure — narrow the audience or change the message category.
* **Audience preview resolves to zero.** Re-run the preview with `channel: "whatsapp"` — the net projection names the emptying cohort (all opted out, no reachable WhatsApp number).
