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

# Choosing a sender construct — pools vs services vs inline senders

> The decision model for sender identification: when to send an inline from, a sender_pool_id, a from_extension, or a messaging_service_id, which of the three inbound layers owns where a reply lands, and the channel rules that constrain the choice.

# Choosing a sender construct

Three different objects identify the sender of an outbound message — a sender pool, a messaging service's default config, and an inline sender on the request body. On the inbound side, three different layers decide where a received message lands. Every one of them is correct somewhere; the hard part is choosing. This page is the decision model: it compares the constructs head-to-head, orders them, and says which one wins each kind of traffic. It defers every per-construct detail to its authoritative page rather than restating it.

## Where this fits

* **This page (the decision)** — which construct to pick for a pattern of traffic, and the precedence rules that decide what happens when you pass several at once. Read this before configuring anything; it is the entry point for sender setup.
* **[Sender resolution](/concepts/sender-resolution)** — the authoritative outbound precedence chain in full depth: every pool strategy with its fail-safes, the per-country pool map, the fallback chain. Read that page when a specific send used an unexpected sender.
* **[Messaging services](/concepts/messaging-services-model)** — the service entity definition: what the bundle contains, when each default fills in, and the lifecycle.
* **[How routing picks a sender](/concepts/sender-and-routing)** — the compact two-sided reference: the outbound chain summary plus the inbound MO rule model in one place.
* **[Inbound message routing](/concepts/inbound-message-routing)** — the tenant-level rule engine for received messages: match types, targets, priority ordering, failure tracking.

## 1. The three sender-identification surfaces

**Sender pools.** A pool holds member senders — DIDs, short codes, alphanumeric sender IDs — and picks exactly one per message by its strategy: `sticky` (a deterministic per-recipient pick, persisted so a recipient keeps one sender), `round_robin`, `random`, or `geomatch` (recipient-country match with a `sticky` fail-safe). Pool members accumulate a health tier from delivery outcomes, and the rotation scheduler swaps a degraded member for a warmed one, so rotation keeps preferring senders that deliver. The pool owns *member-picking*: given a set of identities, choose one. Everything about strategies, health, and fail-safes lives on [Sender resolution](/concepts/sender-resolution).

**Messaging services.** A service is a named bundle of defaults — a default sender pool, a per-country `country_sender_pools` override map, a status callback, a validity period, an inbound webhook, an opt-out list, a per-second throughput cap (`mps_cap`), and behaviour flags. It has no member-selection logic of its own; it *points at* pools, and the pool it lands on does the picking. The service owns *defaults*: it fills in only what the request left blank. The entity definition, the per-country map, and the throughput cap live on [Messaging services](/concepts/messaging-services-model).

**Inline senders.** Two shapes, both passed on the request body and both beating every service default: a bare `from` (a number your tenant owns, a short code, or a registered alphanumeric sender ID) and `from_extension` (a UCaaS extension resolved to the DID on that extension). An inline sender owns *this one send* — no rotation, no defaults, no bundle.

They are not the same object, and none of them is the inbound side. Received messages (MO) are routed by three separate layers — tenant-level inbound rules that match on number, sender, keyword, or regex and dispatch to a webhook, queue, inbox, team, or SMS menu; the messaging service's own inbound webhook binding for its attached numbers; and the per-DID hook underneath both. Those layers are covered on [Inbound message routing](/concepts/inbound-message-routing) and [How routing picks a sender](/concepts/sender-and-routing); section 2's decision tree says which one should own a given reply pattern.

## 2. The decision tree an operator actually runs

Work through these questions in order; the first yes wins.

* **Is this a UCaaS per-user send?** → `from_extension`. The extension's DID is the sender. It cannot combine with a pool id — passing both returns `422`.
* **Should this one send use exactly one fixed identity, no rotation?** → inline `from`. It beats every service default and the fallback chain. Pass it *alone*: if you also pass `sender_pool_id`, the pool wins and your `from` is silently ignored.
* **Should traffic rotate across a fleet of senders?** → `sender_pool_id` on the request, or the pool a messaging service points at. Passing a pool id on the request also beats the service's own pool config outright.
* **Do different countries need different sender fleets from one integration id?** → a messaging service whose `country_sender_pools` maps each country to the pool maintained for that corridor. The per-country override beats the service's default pool — but only when the request carries no `sender_pool_id` of its own. An explicit pool on the body skips the map entirely.
* **Do you need a bundle beyond the sender — status callback, validity period, inbound webhook, per-second send cap, opt-out list?** → a messaging service. Even when its sender config is the least interesting part, one id gives you the rest. Remember the rule: a service is fill-in-only-on-blank — it never displaces anything the request set.
* **Is it a plain two-way reply on an existing conversation, with no opinion about the sender?** → pass nothing. The conversation's sticky sender pins the reply to the DID the contact originally texted.

Inbound side, three layers own where a reply lands; pick one per pattern:

* **Tenant-level inbound rules** — the conditional layer. "Bodies containing `appointment` go to the scheduling webhook; messages to the VIP line go to the VIP inbox." First-match-wins in priority order; exceptions at lower priority values than catch-alls.
* **The messaging service's inbound webhook** — the per-numbers address layer. Attach numbers to the service; everything arriving on them POSTs to `inbound_webhook_url`. This sits *underneath* the tenant rules — a matching rule is consulted before the service binding fires.
* **The per-DID hook / default path** — the catch-all. When no rule matches and no service binding claims the number, the message persists to the inbox and fires the `message.received` event. Keep this layer for the fall-through; add rules and service bindings for the exceptions.

## 3. Channel guardrails that override the tree

* **WhatsApp has no pools.** Meta owns the sender — it is your WABA phone number. A business-initiated message outside the 24-hour session window must use an approved template. Sender pools, `from`, and `from_extension` do not exist on this channel; the decision tree applies to SMS.
* **US and Canada corridors reject alphanumeric MT.** A `+1` destination can never be sent from an alphanumeric sender ID. Keep numeric-only pools for `+1` entries in any per-country map, and expect the platform fallback to substitute a phone number for `+1` recipients if nothing resolves earlier.
* **Two-way SMS replies pin to the original DID.** When you reply on an existing conversation without naming a sender, the sender is the DID the contact texted. If your traffic depends on contacts texting the same identity back, a rotating default sender breaks that; use a `sticky` pool or reply on the conversation.

## 4. Failure shapes to plan for

* **`422 SENDER_POOL_EMPTY`** — the pool has no members. Add at least one DID or sender ID before routing traffic through it.
* **`422` on mutual exclusion** — `from_extension` with `sender_pool_id` is never both. Send exactly one sender construct per request.
* **Ambiguous-body precedence** — `from` + `sender_pool_id` is not an error; the pool wins silently. A send that names both does not fail — it surprises.
* **The `+1` fallback substitution** — with no explicit sender, a US/Canada recipient gets a platform phone number even when your org default is alphanumeric, because `+1` carriers reject the shared alphanumeric sender. Pin an identity (`from` or pool) when the sender matters.
* **`404 MESSAGING_SERVICE_NOT_FOUND`** — a send referencing a deleted service id fails from the next request. There is no silent skip-a-service path.

## 5. When you should not use a pool or a service at all

* **Voice-only DIDs** — a pool is an SMS construct provisioned for its members' messaging capability. Numbers that exist for voice do not belong in one; pass the voice DID as an inline `from` only if it is genuinely SMS-capable.
* **One fixed sender forever** — if the answer to the tree's rotation question is durably "no rotation," an inline `from` is the whole model. A one-member pool is a stillborn pool.
* **UCaaS extension traffic** — per-user business SMS runs on `from_extension`; the pool/service layer is never in that path.

## Worked example — US + GB order notifications

A tenant sends order updates to US and GB customers and wants replies threaded per country with sensible sender hygiene. Three candidate designs, chosen against the tree:

**Option A — inline `from`.** The US traffic goes out from a single owned DID, GB from a single registered alphanumeric sender ID (where GB corridors accept alphanumeric). Simple and correct — until the fixed sender degrades or volume needs spreading, and there is nothing to fail over to.

**Option B — a `sticky` pool.** Two pools: `pool_us` holding US DIDs, `pool_gb` holding the alphanumeric sender plus UK numbers, both `sticky` so each recipient keeps one sender. Rotation-ready and corridor-safe — but two pool ids to pass per request.

**Option C — a messaging service with `country_sender_pools`.** One service `ms_orders` with `country_sender_pools: { "GB": "pool_gb" }` and default pool `pool_us`. Every send passes just `messaging_service_id: "ms_orders"`: GB recipients take the GB override pool, US recipients fall to the default pool, and the service's inbound webhook handles replies on the attached numbers. This is the chosen design — one integration id, per-country sender hygiene, and the rule from the tree: "different countries need different fleets from one integration id → service with a per-country map."

The same `to` a GB customer, one request through that service:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/messages/sms \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+447700900123",
    "messaging_service_id": "ms_orders",
    "body": "Your order shipped"
  }'
```

The map lookup resolves `GB → pool_gb`; `pool_gb` picks one member. A US recipient on the same body pattern instead takes the service's default `pool_us`. Either way, naming a `sender_pool_id` on the body would skip the map entirely — the tree's precedence rules are what keep the interpretation unambiguous.

## See also

* [Sender resolution](/concepts/sender-resolution) — the authoritative outbound precedence chain in full depth
* [Messaging services](/concepts/messaging-services-model) — the service entity definition and lifecycle
* [How routing picks a sender](/concepts/sender-and-routing) — the compact two-sided reference (outbound chain + inbound MO rules)
* [Inbound message routing](/concepts/inbound-message-routing) — the tenant-level inbound rule engine
* [Inbound message resolution](/concepts/inbound-message-resolution) — the tenant-ownership layer underneath inbound routing
* [Sender pools guide](/guides/sender-pools) — create pools, preview picks, read health
* [Messaging credentials & services](/api-reference/messaging-credentials) — the full service endpoint reference
* [Send and receive messages](/guides/send-receive-messages) — end-to-end walkthrough
