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

# Subscribe-many broadcast: one body to many recipients in the notify composer

> Send one shared body to a large recipient list in one POST via the Messages → Multi-channel notify composer — paste a CSV of channel→address bindings, import an audience segment, let the composer dedupe identical pairs, pin a per-channel sender, and read the per-binding ok/failed table. The fan-out broadcast loop, distinct from cascade.

# 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](/guides/notify-cascade-failover). 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](/flows/overview)), when the recipient set is a live segment that should re-evaluate at send time (use a [campaign](/api-reference/endpoints/campaigns)), or when every recipient is on the same channel and a single-channel surface is simpler ([Batch SMS](/guides/messages-batch-sms) for SMS, [email lifecycle](/guides/email-lifecycle-guide) 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](/guides/contact-lists-static-targeting) for hand-curated rosters and [Build CDP segments](/guides/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](/guides/notify-cascade-failover).

## 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](/guides/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](/channels/sms) — 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](/guides/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:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/notify/" \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Idempotency-Key: $BATCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "body": "Our maintenance window starts Sunday 02:00–04:00 UTC. No action needed.",
    "from": "CareClinic",
    "bindings": [
      { "channel": "email",    "address": "ada@customer.example" },
      { "channel": "sms",      "address": "+14155552671" },
      { "channel": "whatsapp", "address": "+14155559876" }
    ]
  }'
```

When every binding queues, the envelope is **200 OK** and each row carries its delivery handle (`message_id`):

```json theme={null}
{
  "data": {
    "notify_id": "nfy_7h2k4p",
    "summary": { "total": 3, "succeeded": 3, "failed": 0 },
    "results": [
      { "index": 0, "channel": "email",    "to": "ada@customer.example", "status": "queued", "message_id": "msg_e01k2x" },
      { "index": 1, "channel": "sms",      "to": "+14155552671",          "status": "queued", "message_id": "msg_s77q1a" },
      { "index": 2, "channel": "whatsapp", "to": "+14155559876",          "status": "queued", "message_id": "msg_w44p0n" }
    ]
  }
}
```

When one row fails — here the recipient opted out of SMS — the envelope is **207 Multi-Status** and only that row carries an `error`:

```json theme={null}
{
  "data": {
    "notify_id": "nfy_7h2k4p",
    "summary": { "total": 3, "succeeded": 2, "failed": 1 },
    "results": [
      { "index": 0, "channel": "email",    "to": "ada@customer.example", "status": "queued", "message_id": "msg_e01k2x" },
      {
        "index": 1,
        "channel": "sms",
        "to": "+14155552671",
        "status": "failed",
        "error": { "code": "RECIPIENT_OPTED_OUT", "message": "Recipient opted out of SMS sends." }
      },
      { "index": 2, "channel": "whatsapp", "to": "+14155559876", "status": "queued", "message_id": "msg_w44p0n" }
    ]
  }
}
```

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](/guides/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.

| HTTP status | What it means | What to do |
| - | - | - |
| **200** | Every binding queued | Wait for per-channel delivery events; nothing to retry |
| **207** | Some queued, some failed | Rebuild the failed rows as a new send; leave the queued rows alone |
| **422** | Nothing queued | Fix the body or addresses and resend the full batch — nothing went out |

## Broadcast vs cascade — pick the shape

The two strategies share an endpoint and a composer but answer different questions:

| Question | Broadcast (fan-out) | Cascade (waterfall) |
| - | - | - |
| One person, many channels — escalate until one delivers | No — every channel fires, one message per channel | Yes — one chain, richest → cheapest, one hop fires |
| Many people, one message each, right now | Yes — one body, N recipients, N attempts | No — cascade waits for undelivered receipts |
| Per-recipient fallback on undelivered | No | Yes — within `fallback_window_seconds` (30s–24h) |
| Response shape | `summary` + per-binding `results[]` | `active` + armed `fallback_chain` |

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](/guides/notify-cascade-failover) walks that loop end to end.

## See also

* [Multi-channel Notify: one message to a mixed recipient list](/guides/multi-channel-notify) — the composer overview, role requirements, and the worked example that mixes fan-out and cascade.
* [Cascade (waterfall) fallback chains in the notify composer](/guides/notify-cascade-failover) — the cascade strategy in depth: chain grouping, the escalation window, and worked API payloads.
* [Idempotent requests](/guides/idempotent-requests) — the `Idempotency-Key` contract every create-call honors, including the replay and reuse-error envelopes.
* [Delivery log](/guides/delivery-log) — search every delivery across channels by `notify_id`, recipient, or provider reference.
* [Static contact lists](/guides/contact-lists-static-targeting) — hand-curated rosters you can import into the composer as the binding set.
* [Sender pools](/guides/sender-pools) — rotate outbound numbers and sender IDs the per-channel sender resolves from.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.