> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Cascade failover policy and one logical message ID across legs

> How an org-level default cascade policy arms every send's fallback chain, how a per-send cascade field overrides it, and how message_group_id binds every hop of a cascade into one readable logical group.

# 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](/concepts/multi-channel-dlr-fallback), which covers the per-request escalation engine these endpoints arm, and [Cross-channel fallback](/concepts/cross-channel-fallback), which covers the separate org-level **capability** chain. The channel send envelopes live in the [Messaging API reference](/api-reference/endpoints/messaging).

## 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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messages/cascade/groups/<group_id>" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

The response returns every leg of the group in send order, oldest first:

```json theme={null}
{
  "data": {
    "group_id": "msg_7f3a…",
    "legs": [
      {
        "id": "msg_01ab…",
        "channel": "rcs",
        "to": "+1555…",
        "status": "undelivered",
        "sent_at": "2026-08-29T10:00:04.000Z",
        "fallback_of": null,
        "fallback_reason": null
      },
      {
        "id": "msg_02cd…",
        "channel": "sms",
        "to": "+1555…",
        "status": "delivered",
        "sent_at": "2026-08-29T10:00:19.000Z",
        "fallback_of": "msg_01ab…",
        "fallback_reason": "primary_rcs_undelivered"
      }
    ],
    "leg_count": 2
  }
}
```

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:

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/messages/cascade/policy" \
  -H "X-API-Key: $ORBIT_API_KEY"
```

```json theme={null}
{ "data": { "policy": { "enabled": true, "channels": ["whatsapp", "sms"] }, "configured": true } }
```

`configured: false` (with a `null` policy) means no policy has been set — not that cascade is disabled.

Replace the policy:

```bash theme={null}
curl -X PUT "https://api.orbit.devotel.io/api/v1/messages/cascade/policy" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "channels": ["whatsapp", "sms"],
    "fallback_window_seconds": 3600
  }'
```

The validated shape:

| Field                     | Type                              | Meaning                                                                                                                |
| ------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `enabled`                 | boolean (default `false`)         | Whether the policy arms new sends at all.                                                                              |
| `channels`                | ordered list, 1–8 hops (required) | Fallback channels consumed head-first after the primary of each send.                                                  |
| `fallback_window_seconds` | integer, ≤ 30 days (optional)     | How fresh a primary send must be for its terminal DLR to escalate. Defaults to the 24-hour engine window when omitted. |

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):

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/rcs" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15551234567",
    "body": "Your appointment is tomorrow at 10:30.",
    "cascade": { "channels": ["sms"], "fallback_window_seconds": 3600 }
  }'
```

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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/sms" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "to": "+15551234567", "body": "…", "cascade": { "enabled": false } }'
```

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](/concepts/multi-channel-dlr-fallback)): 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](/concepts/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](/guides/notify-cascade-failover)) 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.

## Related

* [Fallback on terminal DLR](/concepts/multi-channel-dlr-fallback) — the escalation engine the cascade policy arms.
* [Cross-channel fallback](/concepts/cross-channel-fallback) — the org-level capability chain — a separate, earlier-resolving fallback plane.
* [Cascade fallback chains in the notify composer](/guides/notify-cascade-failover) — per-recipient waterfall chains in one notify call.
* [Message status DAG](/concepts/message-status-dag) — which statuses are terminal and therefore trigger escalation.
* [Messaging API reference](/api-reference/endpoints/messaging) — channel send envelopes the `cascade` field rides on.
