Skip to main content

Voice broadcast with SMS fallback, threaded back into the Inbox

This walkthrough composes the whole voice → SMS fallback campaign end to end: route the waterfall, build the payload, clear the compliance gates, launch, read the per-hop receipt, and receive SMS replies in the Inbox. It stitches the broadcast side (voice broadcasts), the fallback pattern (channel fallback recipe), and the reply loop into one run. Use this page when you want a voice call to lead and an SMS to catch recipients the call could not reach — appointment reminders, outage notifications, delivery windows — with replies landing where your team answers them. For the dashboard one-shot workflow with no fallback, stay on the voice broadcast guide. For OTP-style voice fallback, use a Verify profile instead of hand-rolling the chain.

1. Why cascade_policy drops voice — route as a /notify waterfall instead

The smart-send resolver filters voice (and fax) out of every stamped fallback chain — the terminal-DLR escalation hook applies the same eligibility, so cascade_policy: { fallback_channels: ["sms"] } can never carry a voice hop on either side of the arrow. The channel fallback recipe calls this out for voice → SMS specifically. The surface that does start on voice is POST /notify in mode: "waterfall": you pass the recipient across two or more bindings[], the binding order is the chain order, and hop 0 fires immediately. The remaining hops arm as the escalation tail and advance only when the active hop reports a terminal failure inside fallback_window_seconds. Two consequences you plan around:
  • Voice-first means voice is synchronous. The voice hop queues in the POST; if it rejects synchronously (unprovisioned sender, a per-channel opt-out, a bad from-number), the waterfall advances in-band to SMS before any DLR exists. The POST response names which binding queued in active and which it skipped in skipped[].
  • The fallback window bounds staleness. An undelivered receipt that lands after fallback_window_seconds does not escalate — the send is too stale to be useful. Set it per recipient cohort, not once globally: a “your appointment is in 2 hours” reminder warrants 300–900 seconds, an outage notice warrants longer (floor 30s, ceiling 24h; default 24h when omitted).
Everything below is tenant-owned: you pick the channel order, the window, the sender numbers, and the cost ceilings per send. Nothing here flips a platform-wide switch. For per-binding boolean fallback triggers (campaign style) instead of an ordered chain, see the fallback chains guide.

2. Compose the broadcast payload — two bindings, one envelope

A two-binding waterfall for one recipient looks like this. Voice is binding 0 (fires first), SMS is binding 1 (armed; fires only on a terminal voice failure inside the window):
What each field does:
  • bindings[] — the ordered DeliveryChain. Hop 0 (voice) carries the spoken script; hop 1 (SMS) carries a shortened copy restated for text — mirror the purpose and include opt-out phrasing on the SMS hop (Reply STOP …), since this may be the recipient’s only touch with the message.
  • fallback_window_seconds: 600 — per-send staleness window. A voice no-answer / busy / failed receipt inside ten minutes fires the SMS; one that returns eleven minutes later does not.
  • metadata.campaign — a shared billing topic stamped onto every hop’s message row, so cost and delivery reporting (GET /notify/:notifyId, campaign exports) attribute both hops to the same campaign rather than splitting them across orphaned message ids.
  • max_price (per binding) — per-step cost ceiling in USD. When the resolved cost of that hop would exceed the cap, the hop is rejected before the wallet is billed, so an expensive hop never fires even once.
  • max_total_price: 0.27 — cumulative cascade cap across every hop of this waterfall. If the running total would exceed it, the remaining tail is trimmed and reported as dropped_for_cost_cap. Fan-out mode ignores it — there is no cascade to cap.
A shared from + shared body on the envelope apply to any binding that omits its own; per-binding body, from, template_name, and max_price win on conflict. In practice the voice and SMS scripts always differ in length and phrasing, so per-binding bodies are the norm in a voice → SMS waterfall. Voice clone and the dialing window. To speak the script in a cloned voice, create the clone first (signed consent + a short sample script — the voice clones guide covers the consent and quality gates), then reference the clone through the voice sender profile the from number resolves against. A voice clone changes the spoken rendering, not the compliance footprint: the federal 8 AM–9 PM recipient-local dialing window and your workspace quiet hours still apply to the voice hop, and neither a clone nor a stock voice can relax them. Guard: a one-binding waterfall is a 422.
A waterfall with one binding has nothing to fall back to, so the API rejects it outright and arms nothing. Send a plain voice-only notification by omitting mode (fan-out of one) — or use the campaigns surface. Fan-out vs waterfall for this pattern. mode: "fanout" (the default) fires every binding at the same time — voice and SMS together. For a fallback you want waterfall, where the SMS only spends money after voice terminally fails; fan-out is for “alert on every channel now” traffic and burns both hops every time.

3. Enforce TCPA and DNC before the voice leg

The voice hops in a waterfall are ordinary outbound calls — every gate the broadcast composer applies applies here, on every recipient in every binding. Clear these before you queue a batch of waterfalls: TCPA federal window (non-relaxable). Every voice hop is stamped against the federal 8 AM–9 PM recipient-local dialing window at dispatch. A recipient whose local time falls outside the window at send refuses with 422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED. This is a schedule fix, not a config fix — no workspace setting relaxes it. For a broadcast-sized batch, schedule the launch so the whole recipient cohort stays inside its own local windows, or let blocked recipients land on the SMS hop (the synchronous rejection advances the chain in-band, and the SMS hop refuses only if SMS quiet hours also block it — see the TCPA posture guide for how SMS/WhatsApp quiet hours differ from the voice federal guard). Workspace quiet hours. On top of the federal window, your quiet-hours configuration bounds the per-recipient dialing window and is checked at send. Verification of what your quiet hours actually resolve to lives under the compliance settings — run it before the launch, not after the first 422. Tenant DNC pre-flight scrub. Suppressed and opted-out recipients are excluded at send time, but batching is cheaper than per-send rejection: run the tenant do-not-call source against your recipient list before launching. A scrubbed-off recipient on the voice hop shows up in the POST response as a synchronous skip with an opt-out code, and on SMS as RECIPIENT_OPTED_OUT when it reaches that hop — either way, the cleaner move is keeping them out of the bindings. The DNC pre-flight scrub guide walks the same scrub at single-recipient and batch scale; check the do-not-call source under Outbound → Audiences before the first batch of the day. Recording-consent acknowledgement. If your voice hop renders into a recorded call (your number’s recording-consent posture), the campaign-style gate is acknowledgement; verify it at the workspace level before the first send.

4. Launch, then read the per-hop receipt

Launch with the POST above. The response confirms which binding is live and what is armed:
A …/notify/:notifyId GET aggregates the hop list as DLRs land — one status, one summed price, and per-hop error codes:
How to read it:
  • Hop 0 voice, hop 1 SMS — the full cascade in one envelope. Only the hops that actually attempted bill: cost.total is the true cost of getting this recipient, not the sum of both hops’ ceilings. price and error_code are per-hop, so a VOICE_NO_ANSWER on hop 0 does not poison the read on hop 1.
  • status: "delivered" names the cascade outcome, delivered_channel: "sms" names who landed it. A cascade reports failed only when EVERY hop terminated non-delivered and no escalation tail is armed; in_progress while any hop is still live or armed.
  • skipped[] at POST time vs error_code here. A binding that rejected synchronously (bad sender, opt-out) never entered the cascade; it is in the POST response’s skipped[], with error.code. A hop that queued and terminally failed at the carrier shows here with error_code on its hop row.
  • truncated: true (only past the hop ceiling) and dropped_for_cost_cap (only when you set max_total_price) — both listed in the cascade receipt when they apply.
The dashboard’s Track a cascade panel (Messages → Multi-channel notify) resolves the same envelope without the API call — one hop list, per-hop cost, the escalated-past-primary flag. For full field reading, the track a cascade guide covers every hop column.

5. Run SMS replies into the Inbox — the reply loop closes the campaign

When the SMS hop is the one that lands, that number can reply — and replies need somewhere to go. The sender number on your SMS binding is the Inbox thread identity: replies to it open (or continue) a conversation with the same recipient pair, so your team answers in Inbox → Conversations instead of a webhook you build yourself. The inbox setup guide wires the number into a conversation surface; inbound SMS routing covers which number answers which kind of traffic. What propagates back, and where it lands:
  • A YES / R reply is a conversation row. The recipient’s YES confirms the appointment; that row belongs in the same Inbox thread the SMS hop sent from, with the campaign metadata intact, so an agent sees the full context (“Acme Dental reminder — replied YES”) without joining message ids. Keyword auto-reply rules can automate the acknowledgement when the reply is a known intent.
  • A STOP reply is an opt-out, not just a message. Same thread, but the opt-out also writes to the tenant suppression list immediately — the next binding you build against this recipient is excluded at send time (voice hop skips it synchronously with an opt-out code, SMS hop refuses RECIPIENT_OPTED_OUT). The opt-out rules guide is the semantics reference; the consent and suppression model concept covers how per-channel opt-outs land.
  • Voice-leg outcomes feed back too. A recipient who pressed a digit on the voice call (confirm / reschedule) never needed the SMS hop — the chain ended delivered at hop 0. That touch does not create an Inbox thread, but it shares the same campaign topic, so reporting unifies both outcomes. A recipient who neither answered the call nor received the SMS inside the window is the genuinely-unreachable cohort — the waterfall is what tells you exactly which recipients those are.
The reply loop is why the voice → SMS waterfall beats a voice-only broadcast for reminders: the SMS hop gives the recipient a channel to answer on, and the Inbox gives your team one queue to answer them in.

6. Sandbox validation before production

Prove the advance before real carriers are involved. A sandbox API key (dv_test_sk_*) turns the recipient’s trailing digit into a deterministic receipt — the sandbox magic numbers table lists every scenario. For this walkthrough, the two scenarios that prove the whole flow are: …3 (undelivered primary → escalation fires) on the voice hop and …2 (delivered) on the SMS hop. Re-run section 2 with a one-binding waterfall to confirm the 422 guard. The sandbox simulates receipts; it never rewrites your chain — what advances in sandbox advances in production.

7. Troubleshooting

  • Voice hop never fires; SMS is immediate. Check the POST response skipped[] — a synchronous rejection (opt-out, unprovisioned sender, bad from-number) advances the chain in-band before any DLR exists.
  • 422 TCPA_FEDERAL_DIALING_WINDOW_BLOCKED on some recipients. Their local time was outside the federal window at dispatch. Schedule per recipient-local time; the window cannot be disabled. The SMS hop is unaffected and still catches them.
  • RECIPIENT_OPTED_OUT on the SMS hop. The recipient opted out of SMS between the voice failure and the escalation firing, or you did not scrub DNC pre-flight. Verify the tenant DNC source and re-run the pre-flight scrub on the next batch.
  • dropped_for_cost_cap names a channel you wanted. Your max_total_price was lower than the cascade’s resolved cost; raise the cumulative cap or drop the expensive hop.
  • SMS replies not threading into Inbox. Verify the sender number is an inbox-routed number (inbound SMS routing); a number with no inbox route keeps replies on webhooks only.
  • The voice hop reports delivered but you expected SMS. An answered call terminates the chain — no SMS fires. That is the waterfall working as designed; the recipient got the message by voice.

See also