Skip to main content

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.failed and a per-binding results[] table — instead of correlating N callback envelopes from N requests.
It does not pay off when the send is one step in a branching conversation (use a flow), when the recipient set is a live segment that should re-evaluate at send time (use a campaign), or when every recipient is on the same channel and a single-channel surface is simpler (Batch SMS for SMS, email lifecycle for email). The composer caps a single call at 10,000 bindings; above that, chunk client-side or move to a campaign.

Composer entry point

  1. Open Messages → Multi-channel notify and click Multi-channel notify to open the composer.
  2. 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.
  3. Write the Message body. It applies to every recipient unless a binding carries its own body override.
The composer accepts up to 10,000 bindings per call — the same ceiling Twilio Notify enforces, kept so a migration off Twilio does not hit a smaller wall.

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.
One body, one list, one send — that is the whole shape.

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 own from 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 fresh Idempotency-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_REUSED so 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 the POST /api/v1/notify call.
The practical upshot for a broadcast: if the network drops between your request and the response — a timeout, a 502, a lost connection — you retry the whole batch with the same key. The bindings that already queued are not re-sent; the ones that did not queue yet are. Never re-POST a partial result with a fresh key, or the queued rows go out twice.

Send — worked request and response

The composer is a visual front on POST /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:
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 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-readable error.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