Channel fallback recipe: build a two-step fallback chain
This recipe walks one fallback chain from setup to verified delivery: pick the surface, build a two-step primary → fallback chain, extend it to three hops when you need a final catch-all, validate the whole path in sandbox with magic numbers, and read the outcome — including the carriers that never participated — from the cascade receipt. Every control below is tenant-owned: you choose the channels, the order, the escalation window, and the cost caps per send. Nothing here flips a platform-wide switch.1. Where a fallback chain is set
Two send-time surfaces arm a chain, and they compose differently:- Send-time smart route —
cascade_policyonPOST /messages/smart-send(or read-only onPOST /messages/route-preview). The smart router picks the primary channel from your capability flags and message shape; the policy shapes only the fallback set, which is resolved and stamped onto the message asmetadata.fallback_channels. A terminal delivery failure on the primary then cascades to the next channel in the list. Messaging channels only —voiceandfaxare filtered out of the stamped chain. - Recipients-and-chain bindings —
mode: "waterfall"onPOST /notify. You pass one address across two or morebindings[]rows; the binding order is the chain order (the DeliveryChain shape). Only the first binding fires. The rest arm as an ordered fallback chain, and the engine advances when the active hop reports a terminal failure insidefallback_window_seconds.
channels[] ladders and Verify profiles.
2. The two-step recipe: primary → fallback
Two hops is the right size for most traffic: one rich or cheap primary, one universal fallback you know will land. Three worked shapes — all tested in sandbox in section 4.WhatsApp → SMS (transactional)
The most common production chain: rich primary, universal fallback. The primary is router-picked; the fallback order is pinned.undelivered or failed on its terminal receipt, the escalation hook fires a new send on sms carrying the same body. If WhatsApp delivers, the chain ends — SMS never fires.
Email → SMS (one recipient, both channels)
For a notify-style send where you hold both an email address and a phone number for one recipient, enter the same recipient across two bindings so the rows merge into one chain. The email hop is cheapest and fires first; SMS is the safety net.Voice → SMS (voice-primary flows)
Voice is dropped from stamped fallback chains everywhere — the smart-send resolver filters it out, and the terminal-DLR escalation hook applies the same eligibility. So a chain that starts on voice and falls back to SMS has to put voice at the front of the binding order onPOST /notify, where hop 0 fires immediately:
sms → whatsapp → voice, say) re-delivers the same code per hop with a templates.voice script, handles per-hop timeouts, and fires verification.fallback_exhausted when every hop fails — none of which you would have to build.
3. The three-step cascade: SMS → voice → email
Add a third hop when the first two are both phone-centric and you need an off-phone catch-all. SMS first (universal, immediate), voice second (gets a ringing phone, not just a notification badge), email last (the permanent record that lands even when the handset is unreachable):GET /notify/:notifyId later shows every attempted hop with its own cost.
fallback: true vs a fallback chain. On campaign-style sends you declare a chain per entry with fallback_on triggers (failed, no_delivery, no_engagement) — a per-hop boolean “this hop may fall back” rather than a chain. Use that trigger style when each entry should independently declare the condition under which the ladder advances (the fallback chains guide covers the trigger semantics). Use the notify waterfall shape (or never set a trigger at all) when the chain is a single ordered list you control end to end. If your send carries no fallback declaration at all, a terminal failure on the primary is final.
4. Validate it in sandbox with magic numbers
Sandbox turns the trailing digit of the recipient into a deterministic receipt scenario, so you can prove the chain advances before it touches a real carrier. Use a sandbox API key (dv_test_sk_* prefix) — the same key shape the sandbox quickstart issues.
-
Fire the primary against a “always fails” number. Trailing digit
3returnssent → undeliveredwithin about a second:Hop 0 targets a failing number (…003→undelivered), hop 1 targets a delivering number (…002→delivered). Save thenotify_idfrom the response. - Confirm the chain advanced. Your sandbox webhook endpoint receives the hop-0 terminal receipt, then the hop-1 send — exactly the advance sequence a real carrier produces. The full digit-to-scenario table is at Sandbox magic numbers.
-
Read the receipt. Resolve the envelope and assert
delivered_channelnames the fallback hop:You wanthop 0failed(orundelivered),hop 1delivered, anddelivered_channel: "sms". If hop 1 still showsin_progress, the window has not closed — poll again. -
Negative test the guard. Re-send step 1 with a single binding. The API rejects a one-binding waterfall with
422and nothing arms — the same guard the composer applies per recipient.
5. Failure reading: non-participating carriers and the cascade receipt
GET /notify/:notifyId returns every hop the chain attempted, each with its own error_code, error_message, and billed price. That per-hop read is how you find a carrier that never participated:
- A hop that never went out — rejected before it armed, or skipped. The receipt’s
skippedlist (at send time) and per-hopfailedentries tell you which binding was rejected and why — for exampleRECIPIENT_OPTED_OUTon a channel the recipient unsubscribed from. The chain advances past it without spending on that hop. dropped_for_cost_cap— channels the chain declared but never attempted because they would have exceeded yourmax_total_pricebudget. A non-participating carrier hops here rather than billing a send you capped.truncated: true— the runaway-envelope ceiling hit and the receipt dropped the overflow. The hops you see are the hops that ran.
See also
- Fallback chains — every fallback surface compared: campaign ladders, per-message policy, Verify profiles.
- Smart-send fallback chains — the resolver behind
cascade_policy. - Track a cascade — read a full envelope from its notify id.
- Pre-launch readiness for fallback chains — probe every step before a campaign launch.
- Sandbox magic numbers — the digit-to-scenario table the sandbox validation above uses.