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

# Per-message cascade policy on smart-send: arm the fallback chain on the router's own pick

> Pass a cascade_policy on POST /messages/smart-send or /messages/route-preview to have the smart router stamp an executable fallback chain (metadata.fallback_channels) the terminal-DLR hook consumes — explicit overrides, auto_fallback off-switches, per-segment learned ordering, and a static-order failsafe.

# 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](/concepts/multi-channel-dlr-fallback) 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:

1. **Primary channel.** Recommended by the smart router from your capability flags (`whatsapp_available`, `email`, `rcs_available`, …), `message_type`, `urgency`, and `cost_optimize` — the same scoring `POST /messages/route-preview` exposes. The policy never overrides the pick; it shapes only the fallback set. An explicit `channel_override` on the request still pins the channel as before.
2. **Candidate chain.** Your explicit `cascade_policy.fallback_channels` when present — a full override — otherwise the router's own fallback chain.
3. **Normalization.** The chain is scrubbed: case-insensitively deduped, trimmed, the primary channel itself and `voice`/`fax` removed. A chain that survives normalization is stamped onto `metadata.fallback_channels`, with `metadata.auto_fallback: true`.
4. **Ordering.** `static` when the chain is used as given; `adaptive` when per-segment learned ordering re-sorted it; `off` when `auto_fallback: false` suppressed fallback entirely. The ordering kind is recorded in the message metadata and on the audit event.

Both `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

| Field               | Type      | Notes                                                                                                                                                                    |
| ------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `fallback_channels` | string\[] | Optional. Explicit ordered chain — a full override of the router's fallback. Names are accepted case-insensitively and the primary channel is dropped from the list.     |
| `auto_fallback`     | boolean   | Optional. `false` suppresses all fallback; any other value (or absence) leaves fallback armed whenever a chain resolves.                                                 |
| `segment`           | object    | Optional tag map. When it names one of `segment_id`, `audience_id`, or `campaign_id`, the resolver consults the learned per-segment ordering for the candidate channels. |

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

When `cascade_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: when `segment` 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 same `cascade_policy` object, so a preview always shows what a send would stamp:

* **`POST /messages/smart-send`** resolves the policy, stamps `metadata.fallback_channels` (+ `auto_fallback`) onto the message row, and dispatches on the router's pick (or `channel_override`). The `cascade.ordering` kind is recorded in the audit trail.
* **`POST /messages/route-preview`** resolves the same policy without sending. The response carries a `cascade` block with `primary`, the `fallback_channels` list, and the `ordering`, 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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/messages/smart-send" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "body": "Your order #8421 shipped — track it here.",
    "message_type": "transactional",
    "urgency": "medium",
    "rcs_available": true,
    "whatsapp_available": true,
    "cascade_policy": {
      "fallback_channels": ["whatsapp", "sms", "email"],
      "segment": { "campaign_id": "cmp_retail_q3" }
    }
  }'
```

Response — 202 (the resolved `cascade` block mirrors what was stamped):

```json theme={null}
{
  "data": {
    "id": "msg_01H...",
    "channel": "rcs",
    "status": "sent",
    "recommendation": { "channel": "rcs", "reason": "Rich content for marketing", "fallback": "whatsapp" },
    "cascade": {
      "primary": "rcs",
      "fallback_channels": ["whatsapp", "sms", "email"],
      "ordering": "static",
      "reason": "Rich content for marketing"
    }
  }
}
```

If the RCS primary comes back `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](/concepts/multi-channel-dlr-fallback). 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_policy` fields are validated with the rest of the request — `segment` must be a string map, `fallback_channels` a 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`/`fax` in 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: false` is sticky.** It wins over a legacy `metadata.fallback_channels` chain your pipeline would otherwise stamp: the resolver emits the explicit off before your metadata bag is merged, so the engagement hook sees the suppression.

<Note>
  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](/concepts/multi-channel-dlr-fallback) (how the chain is consumed) and the [Smart route preview guide](/guides/smart-route-preview) (router scoring behind the recommendations).
</Note>
