Skip to main content

Cascade failover policy and logical cascade groups

A message-level cascade is a set of physical sends bound to one logical message ID: the primary send plus every fallback hop that fired when the previous hop couldn’t deliver. Orbit decides which chain a send escalates through from up to three sources, in a fixed precedence — and stamps one message_group_id on all of them, so the whole run is readable as a single group. This page covers the message-level cascade model: the org default policy, the per-send override field, how a hop escalates, and how to read a finished cascade as one group. It complements Fallback on terminal DLR, which covers the per-request escalation engine these endpoints arm, and Cross-channel fallback, which covers the separate org-level capability chain. The channel send envelopes live in the Messaging API reference.

One logical ID, many legs

When a cascade resolves for a send, Orbit stamps a message_group_id on the primary and carries the same value forward onto every hop it escalates to. Each hop is a first-class message with its own id, channel, status, timing, and billing — but the group id never changes across the chain, so the run is readable end to end as one logical message. Read a group back with:
The response returns every leg of the group in send order, oldest first:
The primary shows fallback_of: null; each hop records the message it fell back from and why (primary_<channel>_<terminal_status>), so you can reconcile “the SMS send that followed the failed RCS send” instead of seeing disconnected messages. The read re-resolves legs from your messages on each call — a partially dispatched cascade simply shows fewer legs, and an unknown group id returns an empty list rather than an error. The read carries messages:read or messages:write scope, like the rest of the messages surface.

The org default policy

Use the policy endpoints when you want every send to escalate through the same chain — SMS → WhatsApp → RCS, or any ordered chain of rich-message channels — without stamping metadata per request. Read the current policy:
configured: false (with a null policy) means no policy has been set — not that cascade is disabled. Replace the policy:
The validated shape: The eligible channel set is closed: sms, whatsapp, rcs, viber, telegram, messenger, instagram, line, apple_messages. Voice, fax, email, and push are never cascade-eligible — their billing and consent semantics differ too much for an automatic hop (an undelivered email turning into SMS is a compliance surface, not a convenience). Validation rejects anything outside the set with a 422 that names each invalid field. A malformed policy stored by hand — for example, from a manual settings edit — degrades to configured: false on read rather than failing sends. The policy stores on the organization itself under messaging settings — there is no separate table to migrate — and the update writes against the single settings key, so sibling messaging settings are never clobbered. Writes carry messages:write scope plus an administrator-or-higher role, and each update records an audit-log entry.

Per-send override

Two sources can supply a chain on a single send, and they beat the org policy in a fixed precedence — raw metadata.fallback_channels first, then the typed cascade field, then the org policy. The first non-empty provider wins; sources are never merged, so a caller’s own chain is never silently over-filed by the org default. Stamp a chain with the typed field on the send body (present on every channel send endpoint):
The typed field accepts the same shape as the org policy (enabled, channels, fallback_window_seconds) and defaults to enabled: true. To opt a single send out of the org policy:
Two more opt-out shapes exist: the engine honors metadata.auto_fallback: false even when a chain is present, and the legacy raw-metadata path (metadata.fallback_channels stamped by your pipeline) always wins over both the typed field and the org policy. Only cascaded sends carry a message_group_id — a normal single-channel send groups to nothing, so you can filter “this recipient cascaded” cleanly.

How a hop escalates

When a chain resolves, the send stamps the fallback metadata exactly once, at send time:
  • fallback_channels — the ordered chain (deduped, with the primary channel removed so a chain never cascades into itself).
  • auto_fallback: true — the explicit opt-in the escalation engine requires.
  • fallback_window_seconds — the freshness cap, when set.
  • message_group_id — generated once per cascade; never re-generated mid-chain.
From there, the terminal-DLR escalation engine owns the advance (see Fallback on terminal DLR): when a terminal failed, undelivered, or rejected receipt lands within the window, the engine dispatches the next channel as a brand-new send, carries the remaining tail of the chain and the group id forward, and stamps fallback_of plus the terminal status as the reason on the incoming hop. A hop-bounding idempotency stamp on the original row guarantees at most one dispatch per message regardless of carrier webhook replays. Crucially, every hop re-enters the canonical send pipeline, so sender resolution, opt-out checks, and quiet-hours/consent gates are re-applied per hop — the recipient’s SMS opt-out blocks an SMS hop even if they accepted RCS, exactly like Cross-channel fallback specifies. Each hop bills on its own channel; there is no separate cascade fee class. Org-policy reads on the send path are non-fatal: a transient org-settings failure never blocks a billed send — the send proceeds with only its declared (or empty) chain.

Reading a finished cascade

Two signals distinguish a complete group from a partial one:
  1. The groups endpoint. GET /messages/cascade/groups/:groupId returns every persisted leg, bounded at 100, ordered by send time. A leg that failed to dispatch is simply absent — the shape tells you exactly which hops ran.
  2. The escalation-past-primary signal. Any leg whose fallback_of is non-null tells you the primary did not deliver and the chain advanced; pairing fallback_of with each leg’s final status tells you where the run ended — a delivered terminal hop closes it, while a terminal status on the last leg means the chain was exhausted.
For a walk-through in the dashboard, the Messages view groups legs primary-first on the same envelope, so operators see one logical message rather than N disconnected sends.

Interaction with the notify composer

The POST /notify endpoint composes per-recipient cascades in a single call — mode: "waterfall" plus fallback_window_seconds — and the dashboard’s Multi-channel notify composer builds those chains visually. That surface (covered in Cascade fallback chains in the notify composer) is unrelated to the endpoints on this page, which govern message-level cascades and the org-level default every single-channel send resolves against. Use the notify composer when you send one body to a mixed recipient list; use the policy and per-send field when you want cascade behavior attached to ordinary messaging sends.