Subscribe-many broadcast in the notify composer
POST /api/v1/notify sends one shared body to a mixed list of recipients across SMS, WhatsApp, RCS, email, push, Viber, Telegram, and Messenger in a single API call. When the list is the point — one announcement, one reminder, one outage notice going to everyone at once — that is a subscribe-many broadcast: one body, N recipients, N delivery attempts, one response envelope. The default delivery strategy, fan-out, fires every binding immediately and in parallel.
The Messages → Multi-channel notify page in the dashboard (route /messages/notify) is a visual composer over that same endpoint. The broadcast loop this guide walks — build the recipient list, dedupe it, pin senders, send idempotently, read the per-binding result — lives there. Cascade (waterfall) is a different shape: there, rows that share an address merge into a per-recipient fallback chain that escalates channel-to-channel only on undelivered receipts. That is covered in Cascade (waterfall) fallback chains. This guide is the broadcast path — every recipient fires now, once, on their own channel.
Every control below is tenant-owned: the recipient list, the sender per channel, and the idempotency key are choices you make per send. Nothing here flips a platform-wide switch.
When subscribe-many changes the ROI
A broadcast pays off when the cost of N separate sends — the N API calls, the N retry paths, the N failure surfaces to reconcile — is higher than one call with one envelope to read. Concretely, subscribe-many beats a per-recipient loop when:- You have a finite list and one shared body — a maintenance window notice to 800 accounts, a shipping update to today’s orders, a password-reset reminder to lapsed sign-ups.
- The list is mixed-channel — some recipients are on SMS, some on WhatsApp, some on email — and the right thing is “deliver on whatever channel each person has,” not “blast everyone on SMS.”
- You want one response to parse —
summary.succeeded/summary.failedand a per-bindingresults[]table — instead of correlating N callback envelopes from N requests.
Composer entry point
- Open Messages → Multi-channel notify and click Multi-channel notify to open the composer.
- Keep the Delivery strategy on Fan-out (the default). This is the broadcast path — every binding fires immediately, in parallel, one delivery attempt each. Cascade is the other card and is covered separately.
- Write the Message body. It applies to every recipient unless a binding carries its own
bodyoverride.
Build the recipient list
The list is one row per(channel, address) binding. Two ways to fill it:
- Paste a CSV of channel→address bindings. Each row is a channel select plus an address field — E.164 phone for SMS, WhatsApp, RCS, and Viber; an RFC-5321 email for email; a device token for push; a handle for Telegram or Messenger. Use Add recipient for more rows. An empty address is dropped before dispatch; a malformed one comes back as that row’s per-binding error, not as a form error.
- Import an audience segment. Pull a saved contact list or a CDP segment into the composer as the binding set — the targeting model behind that is Static contact lists for hand-curated rosters and Build CDP segments for rule-evaluated membership. The segment resolves to a set of
(channel, address)pairs the composer treats exactly like a pasted list.
Deduplication
The composer groups identical(channel, address) pairs before dispatch. If the pasted list — or the segment export — carries the same person twice on the same channel (a duplicate row, a contact who appears in two overlapping segments), the duplicate is collapsed to one delivery attempt for that pair. Two bindings that differ only in channel (say whatsapp and sms for the same phone) are not duplicates — they are two distinct deliveries on two channels, and in fan-out both fire.
This is the one place the broadcast path diverges from the cascade path. In cascade, rows that share an address merge into one per-recipient chain (richest → cheapest) so the engine escalates across channels for one person. In a broadcast, the unit is the pair, not the person — a recipient reachable on two channels gets two messages, one per channel. If you want “one message, one person, best channel wins,” that is cascade — see Cascade (waterfall) fallback chains.
Sender pinning
Each binding runs through the same delivery pipeline as a single send on that channel, which means sender resolution applies per binding. Leave the Sender field blank and the messaging router resolves a per-channel sender for you — a number from your sender pools for SMS and voice, a verified WhatsApp Business number for WhatsApp, a domain for email. Set the shared Sender field and it applies to every binding that does not carry its ownfrom override; set a per-binding from and it wins for that row.
For SMS specifically, the resolved sender comes from the pool you configured under SMS channels — a sticky number for conversation continuity, a geo-matched local number for deliverability, or a round-robin rotation for volume spread. Pinning a sender here is the same control as pinning one on a single SMS send; the broadcast just applies it across N bindings at once.
Idempotency across retries
Every send from the composer carries a freshIdempotency-Key header, generated per send batch. The contract is the same one every Orbit create-call honors — covered end to end in Idempotent requests:
- Same key, same body retried within the TTL returns the original response with a replay marker — no second send, no double billing.
- Same key, different body is rejected with
409 IDEMPOTENCY_KEY_REUSEDso a retry that drifts from the original payload can’t silently land a second send. - The SDKs auto-generate the header on every non-GET request; from the composer it is attached for you, and from your own code you pass
Idempotency-Key: <your-key>on thePOST /api/v1/notifycall.
Send — worked request and response
The composer is a visual front onPOST /api/v1/notify; the same call works from code. Fan-out is the default, so mode can be omitted. This call broadcasts one body to three recipients on three channels:
message_id):
error:
status: keep the queued rows, and rebuild only the failed ones as a new send. Never re-POST the whole batch — that double-sends the rows that already went out.
Reading the result
The composer renders the per-binding outcome as an ok/failed table — one row per binding, each carrying a status chip. 200 means every binding queued; 207 Multi-Status means some queued and some failed, with each failed row showing a human-readable reason (opt-out, quota, bad address) and a machine-readableerror.code on hover; 422 means nothing queued — every binding failed synchronously, nothing went out, and a full retry is safe.
A queued row is not a delivered message yet. Terminal delivery arrives through the usual per-channel events — delivery receipts for SMS, WhatsApp, and RCS; provider callbacks for email and push. For a full-batch failure (422, or a 207 where every row failed), the same records surface in Delivery Log, where you can search by notify_id, recipient, or provider reference and see the per-binding failure reason in one mixed-channel result list.
Broadcast vs cascade — pick the shape
The two strategies share an endpoint and a composer but answer different questions:
If the broadcast is the shape, stay on fan-out. If the cost of a missed delivery on one channel is high enough that you want a safety net per person, switch to cascade — Cascade (waterfall) fallback chains walks that loop end to end.
See also
- Multi-channel Notify: one message to a mixed recipient list — the composer overview, role requirements, and the worked example that mixes fan-out and cascade.
- Cascade (waterfall) fallback chains in the notify composer — the cascade strategy in depth: chain grouping, the escalation window, and worked API payloads.
- Idempotent requests — the
Idempotency-Keycontract every create-call honors, including the replay and reuse-error envelopes. - Delivery log — search every delivery across channels by
notify_id, recipient, or provider reference. - Static contact lists — hand-curated rosters you can import into the composer as the binding set.
- Sender pools — rotate outbound numbers and sender IDs the per-channel sender resolves from.