Skip to main content

Cascade (waterfall) fallback chains in the notify composer

POST /api/v1/notify sends one body to a mixed list of recipients across SMS, WhatsApp, email, RCS, push, Viber, Telegram, and Messenger in a single API call. The Messages → Multi-channel notify page in the dashboard is a visual composer over that endpoint, so you can build the recipient list and pick how it delivers. This guide covers the two delivery strategies the composer offers, how cascade groups your rows into a per-recipient fallback chain, how to bound the escalation window, and how to read what came back. Everything here is a tenant-owned control — the chain order and the window are choices you make per message.

When to pass a mode — fan-out vs waterfall

The composer maps onto two values of the mode field:
  • Fan-out (default). Every binding sends immediately, in parallel. One delivery attempt per binding; there is no retry-across-channels. Use it for general announcements where one channel — maybe two for redundancy — is enough and you want the message out everywhere at once.
  • Cascade (waterfall). Only the first binding fires now. The rest ride along as an ordered fallback chain, and the delivery engine advances to the next hop only when the active hop reports back undelivered inside the freshness window. Use it for time-critical messages (a one-time passcode, an appointment reminder, an outage alert) where the cheapest reliable channel should win and the expensive ones are a safety net, not a scatter-blast.
Order the chain richest → cheapest (e.g. rcs → whatsapp → sms → email). The binding order in your request is the escalation order. Omit mode and the API treats it as fanout.

Build a cascade in the composer

  1. Open Messages → Multi-channel notify and click Multi-channel notify to open the composer.
  2. Set the Delivery strategy to Cascade (waterfall). The composer relabels the recipient list to “Recipients & chain”.
  3. Enter one recipient per row (channel + address). To give a recipient a fallback chain, enter the same address on two or more rows with different channels — rows that share an address merge into one chain. The leftmost column numbers each row so the escalation order is visible.
  4. (Optional) Set the Escalation window (seconds). This bounds how late an undelivered delivery receipt (DLR) may still trigger the next hop: minimum 30 seconds, maximum 24 hours. Leave it blank to use the 24-hour default. A value outside that range is dropped when the payload is built, so a mistyped window never blocks your send — and the API would clamp it to the floor/ceiling anyway.
  5. Read the per-recipient chain preview under the list. Well-formed chains (2+ hops) render as EMAIL → SMS → WHATSAPP. A recipient entered once (or a duplicated address with only one channel) shows an amber “Add another channel for failover” callout, and the composer blocks the send with an error naming the offending address while you fix it.
  6. Finish writing the body, optionally a sender, and click Start cascade.
The preview exists so a mistyped address or a single-hop recipient is visible before dispatch, not as a silent no-fallback chain.

The single-hop guard

Cascade without a second hop degrades silently to fan-out semantics: the first hop fires and there is nothing left to escalate to. The composer therefore requires at least two channels per recipient in waterfall mode and blocks the send otherwise. (The API’s own refine only requires ≥2 bindings total for waterfall mode — the per-recipient check here is stronger and lives in the composer.) When you build payloads in code rather than the composer, apply the same rule yourself.

Fan-out send — worked request and response

Fan-out is the default, so you can omit mode entirely. This call targets three channels on one address:
When every binding queues, the envelope is 200 OK and each row carries its delivery handle (message_id):
When one row fails — here the recipient opted out of SMS — the envelope is 207 Multi-Status and only that row carries an error:
Branch per row on status: keep the queued rows, and retry or rebuild only the failed ones. Never re-POST the whole batch — that double-sends the rows that already went out.

Cascade send — worked request and response

Pass mode: "waterfall" and bind the escalation window explicitly. Only hop 1 fires on this call; hops 2–3 arm and wait:
A 200 OK returns a different shape than fan-out: active is the hop that fired, skipped lists any bindings rejected synchronously before the active hop queued, and fallback_chain is the armed tail in escalation order:
Only hop 1 fires now — hop 2 fires only if the active hop comes back undelivered within the 300-second window. If every binding rejects synchronously, the envelope is 422 — nothing armed, no DLR pending, so fix the addresses and resend. mode defaults to fanout when omitted; fallback_window_seconds accepts 30–86400 and defaults to 86400 (24h). Every hop still runs through the messaging router — sender resolution, opt-out, and compliance checks apply per hop, matching each destination’s reachability rules. The full field specification (per-binding overrides, shared template_name / template_params, response envelope) is in the /notify endpoint reference.

SDK variants — the same cascade in code

/notify has no typed SDK helper yet, so both SDKs go through the low-level request helper (the same auth, retry, and error model as the typed methods). Both read ORBIT_API_KEY from the environment:
Apply the same guardrail from the composer in code: in waterfall mode, pass at least two bindings per recipient, and keep fallback_window_seconds in the 30–86400 band. The API refines a whole-request single-binding cascading payload with 422, but the per-recipient “two channels minimum” rule lives in the composer — enforce it yourself on this path.

Errors

A queued envelope can still have rejected hops inside it, and the failure surface splits across three codes:
  • 400 (validation envelope) — a malformed request never reaches the wire: mode not one of fanout/waterfall, a fallback_window_seconds outside 30–86400, a waterfall with fewer than two bindings, or a body with no body/template_name anywhere. The JSON error.details names the offending field.
  • 207 (fan-out only) — partial per-row failure. The results[] row carries error.code — a per-channel rejection such as RECIPIENT_OPTED_OUT, a bad address, or a missing sender — while the other rows queued.
  • 422 — nothing queued: every binding failed in fan-out, or every binding rejected synchronously in waterfall. Rebuild the payload and resend; the batch is safe to retry because nothing went out.
The compliance rejections your tenant owns — opt-out entries, quiet-hours windows, country-level reachability — surface as the per-row error.code values above rather than as blanket failures.

Reading the result

The screen tells you whether each row queued and whether the strategy badge says Fan-out or Cascade:
  • 200 OK — every binding succeeded (fan-out), or at least one binding queued (waterfall). The row list shows a per-row chip: Queued (fan-out) — it fired now; Queued (waterfall) — this hop is the one that fires first, and any delayed deliveries for still-pending hops have not happened yet.
  • 207 Multi-Status (fan-out only) — some rows queued and some failed. The row chips show Failed with a human-readable reason (opt-out, quota, bad address); hover a row to see the machine-readable code. Branch per row — never blanket-retry the batch.
  • 422 — in fan-out every binding failed; in waterfall every binding was rejected synchronously. Nothing was queued; fix the body/addresses and try again.
The composer’s summary strip counts Total / Queued / Failed, and in cascade mode adds the note “Only the first hop per recipient is firing now — the rest escalate automatically if it comes back undelivered.” A queued cascade row is not a delivered message yet; have your webhook subscriber for the delivery-event stream treat those as armed, not settled.
The dashboard badge “cascade queued — only the first hop fired” means exactly that. A queued row next to an overwrite from an earlier run is fine; treat delivery (confirmed DLR) as the terminal signal, and use fan-out when you cannot tolerate an ambiguous queued-only result.

When to use which — a decision shortcut

After you send, resolve the resulting envelope by its notify id — hop chain, per-hop cost, and the escalation flag — with the Track a cascade panel.

See also