Skip to main content

Fallback chains: pick and configure channel cascades across messaging and OTP

A fallback chain is an ordered list of channels your outbound traffic tries in sequence: when one channel cannot deliver (or the recipient never engages), the traffic moves to the next channel in the list instead of dying. Orbit has three places you can build such a chain, and this guide is the one page that walks through all three and tells you which to use:
  1. Campaign-level chains — the channels[] array on a campaign (failed / no_delivery / no_engagement triggers + a fallback_after_seconds window per non-final entry). Best for marketing blasts, drips, and journeys. Covered in the campaign end-to-end guide under “Channel fallback ladder”.
  2. Per-message cascade policy — a cascade_policy object on POST /messages/smart-send (or read-only on POST /messages/route-preview) that resolves an explicit or router-suggested fallback set and stamps it onto the message metadata as fallback_channels. Best for transactional traffic. Full detail in the smart-send fallback chains guide:
The 202 response echoes the resolved chain so you can confirm what got stamped:
If the rcs primary comes back undelivered on the terminal DLR, the escalation hook fires a new send on whatsapp carrying the ["sms"] tail, and so on until the tail empties.
  1. Verify profile step-up chains — the ordered channels array on a Verify profile (sms → whatsapp → voice, and so on) that a background engine advances when the previous channel cannot deliver the code. Best for OTP and MFA.
The two concept pages — Cross-channel fallback (org-level reachability chain) and Fallback on terminal DLR (post-DLR per-request escalation) — describe the underlying engines the surfaces above compose with. You do not need to read them to configure any of the three; they explain what happens when the surfaces run.

1. Choose the chain type per use case

Rule of thumb: campaigns for marketing, cascade_policy for one-shot transactional, Verify profiles for OTP. They compose — a campaign can roll out its channels[] ladder for the bulk, while each sent message inside the campaign can additionally opt into per-message escalation; only opt into the per-message shape when the org chain would not already cover you.

2. Channel-ordering rules (all three surfaces)

  • First entry = the primary channel. For campaigns the top-level channel must equal channels[0].channel. For Verify profiles the send body’s channel should equal channels[0] (a different one is tacked on front, not an error). For cascade_policy the smart router picks the primary and the policy shapes only the fallback set — never the primary.
  • The terminal entry carries no fallback fields. In a campaign chain, the last object in channels[] must not carry fallback_on / fallback_after_seconds — it is the hop that finishes the chain. The platform enforces this at validation; a chain whose last entry still declares a fallback trigger is rejected with a 422 naming the entry.
  • Channel-specific fields live only on that channel’s entry. whatsapp_template is legal only on a channel: "whatsapp" entry — never on the sms or primary entry of a mixed chain. (Same rule holds for rcs_template, subject on email entries, etc. — see the campaign guide’s field table.)
  • Duplicate channels are rejected. Depth and no-duplicate rules are enforced at create/update time (campaigns) and profile save time (Verify).
  • Messaging-only entries in per-message cascades: voice and fax are filtered out of cascade_policy.fallback_channels; Verify chains additionally admit voice (with a templates.voice script containing {{code}} — mandatory) plus email, viber, telegram, rcs, flashcall.

3. Worked example A — WhatsApp-first → SMS (campaign chain)

The shape you reach for on a marketing promo: charge the richer channel first, fall back to the universal one on terminal failure. Reuse the pattern from the WhatsApp campaign launch playbook:
fallback_on: "failed" advances on a terminal DLR (failed / undelivered / rejected); fallback_on: "no_delivery" advances when no DLR arrives within fallback_after_seconds; fallback_on: "no_engagement" advances when delivery succeeded but no engagement event (open / read / reply) was recorded in the window — the SMS hop still costs a send. The last sms object deliberately carries no fallback fields: it is the terminal hop.

4. Worked example B — OTP step-up (Verify profile)

An SMS → WhatsApp → voice chain; the same code is re-delivered on each hop, and voice requires a templates.voice script with {{code}}:
Pass the returned profile_id on /verify/send; the initial send tries sms, and a background engine advances to whatsapp, then voice, on a delivery failure or a channel_timeout_seconds timeout (10–600 s, default 60 s) — see the Verify fallback chains guide for the advance engine and the verification.fallback_triggered / verification.fallback_exhausted events.

5. Failure semantics: what counts as no_delivery vs no_engagement, and when the chain stops

  • failed (campaign chains): a terminal DLR on the current hop — the carrier replied with failed, undelivered, or rejected. Cheapest trigger: the chain moves only when a hard failure is known.
  • no_delivery (campaign chains, and the Verify advance engine’s timeout branch): no terminal DLR arrived within fallback_after_seconds (campaign) or channel_timeout_seconds (Verify). Use it for channels whose carriers routinely leave you hanging, or for WeChat where template sends may never produce a delivery callback.
  • no_engagement (campaign chains only): the message was delivered but produced no open/read/reply event within the window. This hop costs a second send and should be used sparingly — measure the engagement criteria carefully before enabling it.
  • Verify profiles advance on either a delivery failure or the timeout (above). no_engagement does not apply to OTP: a delivered code that is not yet approved is not a deliverable-channel failure.
  • The chain stops when a hop succeeds, when the terminal hop is reached, or (Verify) when every hop has failed (verification.fallback_exhausted fires). Every hop’s outcome is attributed on its DLR webhook under its own message id (per-message cascades / campaign chains) or the verification.fallback_triggered event (Verify), so a per-hop attribution read always shows the final outcome per hop, not one aggregated status.
  • A per-message fallback never re-arms an expired message: fallback_window_seconds (default 24 h) caps how fresh the original send must be for a terminal DLR to escalate (see Fallback on terminal DLR).

6. Cost: per-hop billing, and cap the whole thing

Every hop is billed independently — the cascade engine treats a fallback advance as a new send on the next channel, and the wallet deducts per hop as the chain advances (for campaigns the per-hop cost comes from the same resolver the dry-run uses; for Verify the channel’s per-message rate applies; for per-message cascades the route-preview cost gates apply). Two bounds you should set explicitly when a chain can grow expensive — documented in Notify cascade cost model:
  • max_price (per-binding or request-level): reject a single hop whose projected price exceeds the cap before billing it — a pricey RCS hop can be skipped without burning the SMS tail.
  • max_total_price: a cumulative budget across the whole waterfall — the chain is trimmed at arm time so the running total of kept hops never exceeds your declared cap.
For campaign chains, set credit_cap_usd_cents on the campaign as a hard spending cap; for Verify profiles, rateLimitPerHour bounds per-recipient spend velocity.

7. Preview a chain before arming it

Two read-only preview endpoints let you check what a chain would do without sending:
  • POST /messages/route-preview — resolves the smart router’s primary pick and your cascade_policy (explicit or router-suggested) and returns the resolved chain under a cascade block with the ordering (static / adaptive / off). Total cost estimate included. See the smart route preview guide.
  • POST /campaigns/:id/preview-smart-send — on a send_time_optimization: "recipient-optimal" campaign, returns per-recipient recommended times plus a next_best_channel hint so you can see what channel each recipient would land on before the bulk send commits. See the campaign end-to-end guide step 6.
For Verify profiles the “preview” is to subscribe to verification.fallback_triggered and run a manual /verify/send against a test recipient before wiring the profile into your login flow.