Per-message cascade policy on smart-send
POST /api/v1/messages/smart-send picks a channel with the smart router and dispatches on it. With a cascade_policy on the same request, the pick also arms a fallback chain: the router resolves your per-message policy into an ordered channel list and stamps it onto metadata.fallback_channels at send time — the exact metadata shape the terminal-DLR escalation hook consumes. An undelivered primary send then cascades to the next channel in the chain instead of stopping there.
Without a cascade_policy, smart-send behaves exactly as before: it sends on the recommended channel with no fallback arm, and a terminal delivery failure stays final unless you stamped fallback metadata yourself.
Set a cascade_policy when a smart-routed send must not die on the first channel — OTP and transactional traffic with reachability risk (RCS-first sends), or marketing sends where you want WhatsApp before SMS rather than a bare SMS hop. Leave it unset (or pass auto_fallback: false explicitly) for recipients whose consent dossier only covers the primary channel.
What the resolver resolves
The resolver runs between the router’s pick and the dispatch:- Primary channel. Recommended by the smart router from your capability flags (
whatsapp_available,email,rcs_available, …),message_type,urgency, andcost_optimize— the same scoringPOST /messages/route-previewexposes. The policy never overrides the pick; it shapes only the fallback set. An explicitchannel_overrideon the request still pins the channel as before. - Candidate chain. Your explicit
cascade_policy.fallback_channelswhen present — a full override — otherwise the router’s own fallback chain. - Normalization. The chain is scrubbed: case-insensitively deduped, trimmed, the primary channel itself and
voice/faxremoved. A chain that survives normalization is stamped ontometadata.fallback_channels, withmetadata.auto_fallback: true. - Ordering.
staticwhen the chain is used as given;adaptivewhen per-segment learned ordering re-sorted it;offwhenauto_fallback: falsesuppressed fallback entirely. The ordering kind is recorded in the message metadata and on the audit event.
smart-send (the dispatch) and route-preview (the read-only check) resolve the policy the same way; the preview returns the resolved chain in a cascade block so you can inspect it before sending.
cascade_policy fields
Channel eligibility: messaging channels only
voice and fax are filtered out of the resolution, matching the fallback eligibility the terminal-DLR hook itself applies. The stamped chain is messaging channels only — sms, whatsapp, viber, rcs, email, and the OTT family. If you pass them, or if the router’s fallback was only reachable via voice (an OTP to a phone-only recipient), the chain normalizes to empty and nothing is stamped.
auto_fallback: false resolves to an explicit off: the send’s metadata is stamped auto_fallback: "false" with an empty fallback_channels list, so even if a tenant-level default chain or an upstream pipeline would stamp one, this send must not escalate. Use it on smart-send pipelines that default to a chain but carry sends that must not hop channels.
Omitting cascade_policy entirely is different: no policy resolves, and the send carries no cascade arm at all.
Segment hints: learned per-segment ordering
Whencascade_policy.segment names one of segment_id, audience_id, or campaign_id, the resolver routes the chain through the learned cascade ranking: your own terminal-sends history over the last 30 days, blended per channel from carrier delivery and read/engagement rates, smoothed so a 1/1 channel can’t outrank a 900/1000 one. Below 20 pooled terminal sends the learned order is not trusted and the static order is kept.
What changes for you: the stamped chain may come back re-ordered (ordering: "adaptive") — never with channels added or dropped. The ranking output is a strict permutation of the candidates, so a re-order can put RCS ahead of WhatsApp for a segment, but it can never invent a hop. The audit event records the ordering kind either way, so you can tell static from adaptive sends in the messages list.
Failsafe: any resolver error falls back to the static chain
Ranking reads historical rows over a rolling window and runs on every smart-send the policy wires. The resolver is built so nothing on that path can stop the send: a ranking failure (or a ranking that returns nothing usable) is caught inside the resolver, and the chain falls back to the static order — so no db handle sits between you and the dispatch. Segment-less policies skip the ranking lookup entirely: whensegment names none of the three fields, resolution is pure and never touches the db.
Where it runs: both smart-send and route-preview
Both endpoints accept the samecascade_policy object, so a preview always shows what a send would stamp:
POST /messages/smart-sendresolves the policy, stampsmetadata.fallback_channels(+auto_fallback) onto the message row, and dispatches on the router’s pick (orchannel_override). Thecascade.orderingkind is recorded in the audit trail.POST /messages/route-previewresolves the same policy without sending. The response carries acascadeblock withprimary, thefallback_channelslist, and theordering, so a planning pass can see the exact chain a send would arm.
Worked example
Send with a three-channel explicit chain. The recipient is reachable on RCS and WhatsApp; we let the router pick the primary but pin the fallback order:cascade block mirrors what was stamped):
undelivered on the terminal DLR, the escalation hook consumes the stamped chain: it fires a new send on whatsapp carrying the remaining tail ["sms", "email"], and so on until the tail empties — the same consume-tail behaviour described in Fallback on terminal DLR. A sender whose pipeline defaults to a chain but must not escalate this one send passes "auto_fallback": false instead, and the metadata lands as explicitly off.
Preview the same payload against POST /messages/route-preview first if you want to confirm the resolved chain; the cascade block returns identically.
Failure modes and edge cases
422 VALIDATION_ERROR.cascade_policyfields are validated with the rest of the request —segmentmust be a string map,fallback_channelsa string array. A chain with zero usable entries passes schema validation but resolves to no arm (nothing stamped).- Primary-echo entries dropped. Passing the router’s eventual pick inside
fallback_channels(e.g.["rcs", "sms"]when RCS wins) is not an error — the resolver drops it from the chain during normalization. voice/faxin the chain are dropped, not rejected. A chain of only voice-plus-fax normalizes to empty; the send proceeds with no fallback arm and a 202.auto_fallback: falseis sticky. It wins over a legacymetadata.fallback_channelschain your pipeline would otherwise stamp: the resolver emits the explicit off before your metadata bag is merged, so the engagement hook sees the suppression.
The resolver never overrides the smart-router pick —
channel_override still wins on the primary channel, and a policy shapes only the fallback set. To read more on the triggering side, see Fallback on terminal DLR (how the chain is consumed) and the Smart route preview guide (router scoring behind the recommendations).