> ## 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.

# Your first multi-channel campaign: SMS with WhatsApp fallback

> Put one fallback-aware campaign on the wire end to end — pick a segment, preview the message, wire the SMS → WhatsApp fallback chain, pass preflight, send, read the delivery log, and measure one KPI.

# Your first multi-channel campaign: SMS with WhatsApp fallback

<Note>
  For the full campaign lifecycle — approval gates, drip and journey shapes, advanced segments, holdout and ROAS measurement — use [Send a campaign end-to-end](/guides/campaign-end-to-end). This page is the shortest complete path to a single send with a fallback chain.
</Note>

One scenario, one page: an **abandoned-cart recovery** nudge that reaches each recipient on **SMS first**, and falls back to a **WhatsApp approved template** (or RCS, if that's live on your account) when SMS hits a terminal failure. One sending sender number, one segment, one preflight pass, one KPI to read back. No agents, no CDP event triggers.

**You will:**

1. [Pick the segment](#1-pick-the-segment)
2. [Preview the message](#2-preview-the-message)
3. [Create the campaign with a fallback chain](#3-create-with-a-fallback-chain)
4. [Run the preflight gates](#4-run-the-preflight-gates)
5. [Send](#5-send)
6. [Watch the delivery log](#6-watch-the-delivery-log)
7. [Measure one KPI](#7-measure-one-kpi)

## Prerequisites

* **An API key with `campaigns:write`** (reads below also accept `campaigns:read`). Create one in **Settings → API Keys**.
* **An SMS-capable sender you own.** Buy one per [Your first SMS, end to end](/guides/first-sms-end-to-end) step 2. For US traffic, complete [10DLC registration](/guides/10dlc-registration).
* **A connected WABA** (WhatsApp Business Account) if you use the WhatsApp fallback — Meta bans inbound-only: connect one per [WhatsApp onboarding](/channels/whatsapp). The WhatsApp hop needs an **approved template** for business-initiated sends.
* **An opted-in segment.** Recipients must have given the right consent for the channel you reach them on. Start from a sandbox key (`dv_test_sk_…`) and swap a live key in for production.

Scope the sender to one number: every step below uses `+18005551234`. Outbound SMS/MMS always terminates through the Devotel wholesale network — the sender field chooses only which of your identities it presents.

## 1. Pick the segment

A campaign on this page targets one segment — a dynamic, filter-based audience — by `audience_type: "segment"` with a `audience_id`. Segments re-evaluate at send time, so an abandoned-cart segment scoped to "cart event in the last 24h" picks up fresh recipients instead of a frozen list.

Count what the segment resolves to before you launch — **`POST /campaigns/audience/preview`** returns `matching_count` plus a 10-contact sample:

<CodeGroup>
  ```bash curl 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": "segment",
      "audience_id": "seg_abandoned_cart_24h",
      "channel": "sms"
    }'
  ```

  ```typescript Node.js theme={null}
  import { Orbit } from "@devotel-orbit/node";

  const orbit = new Orbit({ apiKey: process.env.ORBIT_API_KEY });

  const preview = await orbit.request("POST", "/campaigns/audience/preview", {
    audience_type: "segment",
    audience_id: "seg_abandoned_cart_24h",
    channel: "sms",
  });
  console.log(preview.data.matching_count);
  ```
</CodeGroup>

Pass `channel` and the response also returns a net projection: gross matches minus suppression rows, minus channel opt-outs, minus contacts with no deliverable address. If `matching_count` is 0, fix the segment before going further. The [CDP segments guide](/guides/cdp-segments) covers segment definition; the [audience pre-flight review guide](/guides/campaign-audience-preflight-review) covers the resolution math.

## 2. Preview the message

Templates use `{{token}}` placeholders resolved per-recipient from contact fields. **SMS primary** carries the campaign-level `message_template`; the **WhatsApp fallback** carries its own approved-template identity on the chain entry (the `channels` array in step 3 — the field binds to the exact chain step because a campaign-level template name is meaningless across heterogeneous channels). Resolve `{{first_name}}` and per-recipient tokens against sample contacts before launch — [campaign personalization preview](/guides/campaign-personalization-preview) walks the response shape; a typo'd `{{frist_name}}` resolves to an empty string at send time instead of failing loudly, so catch it here.

## 3. Create with a fallback chain

The `channels` array is the fallback chain: an ordered list of up to 8 channels. Each non-final entry carries a `fallback_on` trigger (`failed` / `no_delivery` / `no_engagement`) and a `fallback_after_seconds` window (60–604,800 seconds). The last entry is terminal and must not carry fallback fields. The top-level `channel` must equal `channels[0].channel`. The full ordering rules live in [fallback chains](/guides/fallback-chains) — this step wires the SMS → WhatsApp case:

<CodeGroup>
  ```bash curl 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": "Abandoned cart recovery — SMS+WhatsApp",
      "type": "blast",
      "channel": "sms",
      "audience_type": "segment",
      "audience_id": "seg_abandoned_cart_24h",
      "message_template": "Hi {{first_name}}, you left something in your cart. Complete checkout in one tap: {{cart_url}} — reply STOP to opt out.",
      "channels": [
        {
          "channel": "sms",
          "order": 0,
          "fallback_on": "failed",
          "fallback_after_seconds": 300
        },
        {
          "channel": "whatsapp",
          "order": 1,
          "whatsapp_template": { "name": "cart_recovery_v2", "language": "en" }
        }
      ],
      "variables": { "from": "+18005551234" }
    }'
  ```

  ```typescript Node.js theme={null}
  const campaign = await orbit.request("POST", "/campaigns", {
    name: "Abandoned cart recovery — SMS+WhatsApp",
    type: "blast",
    channel: "sms",
    audience_type: "segment",
    audience_id: "seg_abandoned_cart_24h",
    message_template:
      "Hi {{first_name}}, you left something in your cart. Complete checkout in one tap: {{cart_url}} — reply STOP to opt out.",
    channels: [
      { channel: "sms", order: 0, fallback_on: "failed", fallback_after_seconds: 300 },
      {
        channel: "whatsapp",
        order: 1,
        whatsapp_template: { name: "cart_recovery_v2", language: "en" },
      },
    ],
    variables: { from: "+18005551234" },
  });
  console.log(campaign.data.id); // cmp_…
  ```
</CodeGroup>

`fallback_on: "failed"` advances on a terminal DLR (`failed` / `undelivered` / `rejected`) — the cheapest trigger, moved only when a hard failure is known. `fallback_after_seconds` is ignored on that trigger but the schema still accepts the value; use it consistently so an operator reading it later can set `no_delivery` without a shape change. `no_delivery` advances when no DLR arrives within the window; `no_engagement` advances on delivered-but-no-open/read — reach for it only after measuring engagement semantics. The WhatsApp hop bills independently per recipient — every chain is per-hop billed, verified in [fallback chains](/guides/fallback-chains) §6.

The response returns the campaign id (`cmp_…`) in `data.id` — hold it for preflight and launch.

### What to skip on your first send

Some surfaces exist for second and third campaigns — defer them deliberately:

* **A/B variants** — [A/B testing](/guides/campaign-ab-testing) splits an audience across two message bodies. Measure a single message first.
* **Quiet-hour per-campaign overrides** — stick to the org-quiet-hours gate (set in [quiet hours configuration](/guides/quiet-hours-configuration)); per-campaign overrides add a second policy to audit. The preflight dry-run projects the skip estimate under the org gate.
* **Holdout control cohorts** — a holdout tells you whether the send beat doing nothing. Readership on one KPI first; the [holdout uplift guide](/guides/campaign-holdout-uplift-measurement) is there when you need it.
* **ROAS attribution** — revenue attribution needs conversion-goal wiring and is covered in [campaign ROAS attribution](/guides/campaign-roas-attribution). A delivery-rate KPI is a faster first read.

## 4. Run the preflight gates

The checklist in [Outbound compliance pre-flight](/guides/send-gates-preflight-checklist) names the gates a campaign send walks. For this single-send flow run the same pass — wallet, opt-out/suppression, quiet hours, frequency caps, compliance profile — and confirm each item below before launch. Copy the box state from that page; the send path will mechanical-422 anything missed:

* [ ] **Wallet is funded and not paused** (`GET /billing/balance` → `outbound_paused: false`).
* [ ] **Suppression list imported and live** — [Opt-out & suppression lists](/compliance/opt-out-suppression).
* [ ] **Quiet hours window chosen** — org gate or campaign fallback window, decided deliberately.
* [ ] **Frequency caps set per channel** — [Frequency caps](/guides/frequency-caps).
* [ ] **Destination compliance profile attached** where the market requires it ([Compliance](/compliance/send-gates)).
* [ ] **DNC check for a sample of recipients** — run `POST /compliance/dnc/scrub` (up to 500 numbers per request) against the segment; clear rows pass.

Then run the read-only **dry-run** — it never mutates state, never touches the wallet, never enqueues a job:

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

  ```typescript Node.js theme={null}
  const report = await orbit.request("POST", `/campaigns/${"cmp_abc123"}/dry-run`);
  console.log(report.data.cost.sufficient, report.data.warnings);
  ```
</CodeGroup>

The `channel_waterfall` bucket reports the primary channel and the fallback shape — verify it shows `sms` primary with `whatsapp` as the declared fallback. A healthy dry-run returns `warnings: []` and `cost.sufficient: true`; anything else names the gate that would block.

## 5. Send

Launch with `POST /campaigns/:id/send` (or set `scheduled_at` for a future batch). A `type: "blast"` campaign sends the whole deliverable audience in one pass at the `throttle_rate` pace (platform default 50 msg/s when the field is 0 or absent). The [throttle guide](/guides/adaptive-pacing) covers pacing; set `throttle_rate` explicitly once you know your volume.

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

  ```typescript Node.js theme={null}
  const launch = await orbit.request("POST", `/campaigns/${"cmp_abc123"}/send`);
  ```
</CodeGroup>

The response `202`s with the campaign back in `running` state. If your org requires supervisor approval the launch returns a pending-approval state instead — check `GET /campaigns/approvals/pending` from the approver's queue.

## 6. Watch the delivery log

Subscribe to `message.delivered` / `message.failed` / `message.received` webhooks on one endpoint before the very first send — the receiver-loop shape is in [Your first SMS, end to end](/guides/first-sms-end-to-end) step 4, and the per-campaign webhook events are in [campaign lifecycle webhooks](/guides/campaign-end-to-end#8-subscribe-to-campaign-webhooks). For a page-into-the-log read, `GET /campaigns/:id/recipients` returns rows with per-recipient status and cursors (`pagination.has_more` / `next_cursor`):

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

Each fallback hop writes its own message id and its own status — a recipient who lands on the WhatsApp hop shows both the SMS `failed` row and the WhatsApp `delivered` row. Group block rows by gate (`opted_out`, `frequency_capped`, `quiet_hours_blocked`) the way the preflight checklist §8 maps them, and fix the audience or the gate — never resend blindly.

## 7. Measure one KPI

Pick one KPI per first send and read it back. For this flow the natural one is **delivery rate** — `GET /campaigns/:id/stats` totals sent/delivered/failed/opened/clicked/replied:

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

The delivered rate divides delivered by sends that reached terminal state (delivered + failed), so a stuck non-terminal `sent` row doesn't drag the number down. Compare that KPI across the primary and fallback hops on the same recipients, not campaign-wide — a healthy fallback chain shows fail-over adds delivered messages that would otherwise have been lost. When the deliverable number looks right and holds on the second campaign, layer the deferred surfaces (A/B, holdout, ROAS) in.

## See also

* [Send a campaign end-to-end](/guides/campaign-end-to-end) — the full lifecycle (draft → preflight → launch → measurement).
* [Fallback chains](/guides/fallback-chains) — campaign ladders, per-message cascade policies, Verify profile step-up.
* [Outbound compliance pre-flight](/guides/send-gates-preflight-checklist) — the gates this page passes.
* [Campaign personalization preview](/guides/campaign-personalization-preview) — resolve recipient tokens before launch.
* [Campaign create wizard](/guides/campaign-create-wizard) — the dashboard path for the same flow.
* [CDP segments](/guides/cdp-segments) — create the abandoned-cart segment this guide scopes.
