Sender pool selection
A sender pool is a named set of member senders — owned numbers, short codes, or alphanumeric sender IDs — with one persisted selection strategy and one health ledger per member. For each message you point at it withsender_pool_id, the pool picks exactly one member as the from. Which construct to build (pool, messaging service, or inline sender) is decided on Choosing a sender construct; where a pool id sits in the full precedence chain against from, services, and fallbacks is owned by Sender resolution. This page is the missing middle: how the pool itself behaves once traffic hits it.
Where this fits
- This page (the pool entity) — what a pool member set is, exactly how each strategy picks, what the health model does to membership, what failure classes fire, and how pool lifecycle events (member add/remove, delete) interact with in-flight selection state. Read this when the question is “I have a pool; how does it actually pick, and how does it keep picking senders that deliver.”
- Sender resolution — where
sender_pool_idwins over a barefrom, where per-country pool overrides sit in a messaging service, and the 3-step fallback chain. This page defers to it for precedence rather than re-telling the chain. - The API reference and guide — the Sender Pools endpoint reference owns request/response schemas; the Sender pools guide walks through creation and preview. This page explains behaviour, so it duplicates neither.
1. What a pool is
Three facts define the entity:- A named member set. Between 1 and 50 sender identities — E.164 long codes, numeric short codes, alphanumeric sender IDs, or a mix. Duplicate entries are rejected on write; the dashboard pool editor also excludes senders another pool already owns, so a sender rotates in one pool at a time.
- One persisted strategy.
sticky(default),round_robin,random, orgeomatch. The strategy is the pool’s own property — change it withPATCHand every subsequent send through the pool picks by the new rule, without touching any send code. - One health ledger per member. Tenant-owned DIDs accumulate a reputation score from delivery outcomes (a daily scheduler recomputes it from DLRs); the pool reads this for auto-rotation, below.
sender_pool_id to this pool and before the message gets a concrete from. The pick it returns is then the identity the rest of the pipeline treats as yours.
2. The four strategies, with exact semantics and fail-safes
Three rules worth internalizing:
- Deterministic within a subset. Both
stickyandgeomatch’s match-branch use the same deterministic hash pick, so “same recipient → same sender” holds inside whatever subset the filter produced. A conversation stays on one local number for geo-matched traffic, not just on any one of all members. - Alphanumeric members are geomatch-hidden. A short code or alphanumeric sender ID has no resolvable country, so it never appears in a
geomatchmatch set — it is only ever picked via the whole-pool fallback when no E.164 member matches. Keep alphanumeric members in a dedicated pool (or accept them as last-resort senders) in corridors where they apply. - One overrides the stored strategy. A parent messaging service with
area_code_geomatch: trueforcesgeomatchon the pool regardless of the pool’s own storedsticky/round_robin/random. Same precedence rule as the wire behaviour — service flag beats pool default — stated once here because it changes how your pool actually picks under a service. Full service semantics live in Messaging services.
3. The member health model
Health is the pool’s closed loop with actual delivery:- Per-member accrual. Each tenant-owned DID member’s health tier (
excellent,good,fair,poor,critical) recomputes daily from a rolling 30-day DLR window. Unknown reputation (never-sent, or a too-thin sample) is deliberately not a swap signal. Sender warming and reputation owns the accrual kernel — this page owns only how the pool reacts to it. - The rotation scheduler. A 6-hour autopilot reconciles pool membership against health: a member that dropped to
poororcriticalis quarantined out of the set and, when a warmed non-degraded replacement exists, substituted one-for-one. - Safety rails on the swap. The planner never empties a pool — a degraded member is only removed when at least one member remains, or kept if dropping it would empty the pool. One replacement member is consumed at most once per tick across all of a tenant’s pools, so two pools cannot both claim the same warmed number.
- Distribution, not just health. Beyond swapping, the same tick flags a pool that is too concentrated for its projected daily volume (risks per-member throttling) by expanding it with warmed members, and surfaces a too-spread pool (a snowshoe-risk pattern) for an operator to consolidate — auto-consolidation is deliberately not done, because removing live sending capacity is operator intent.
4. Failure classes: empty pool and no healthy members
The one hard failure a pool can produce isSENDER_POOL_EMPTY (422): the pool exists but has zero member senders. (The country filter can never empty a pool — geomatch falls back to sticky over the whole member set instead, per section 2.) The two shapes of the failure:
- Empty pool at send time.
sender_didsis empty (no members ever added, or all members removed). Sends through the pool fail fast with422 SENDER_POOL_EMPTY. - Pool miss. A deleted pool id, or a pool in another tenant, returns
404 SENDER_POOL_NOT_FOUND— same class, different cause.
SENDER_POOL_EMPTY: a pool whose every member is degraded still resolves — sending from a degraded member beats failing the send. fully_degraded on the health endpoint is the operator-visible warning that the pool has no healthy member to prefer.
5. Lifecycle: create → members → preview → health drain → delete
- Create —
POST /api/v1/messaging/sender-poolswith a label, 1–50 member sender ids, and an optional strategy (defaultsticky). Owner/admin only. - Add/remove members —
PATCHreplaces the member list. Removing a member that held in-flight sticky assignments does not strand anyone: the next send through the pool detects the stale assignment (the stored sender is no longer in the member set), re-picks deterministically, and rewrites the row. The replaced list propagates to the send path within ~10s (the pool row is cached briefly). - Preview —
GET .../sender-pools/{id}/preview?recipient=<E.164>answers “which sender would this recipient get” without committing anything: no sticky row written, no round-robin counter advanced, no production routing mutated. Use it to verify exactly which sender a recipient would get before wiring the pool into a campaign; the dashboard pool editor exposes the same dry run visually. - Health drain —
GET .../sender-pools/{id}/healthrolls up per-member tiers and counts (at_risk,fully_degraded); the dashboard rotation-alerts panel renders the same roll-up. - Delete —
DELETE .../sender-pools/{id}removes the pool and its sticky-assignment rows. New sends referencing the id start failing404 SENDER_POOL_NOT_FOUNDfrom the next request; messages already sent are unaffected. Re-map usages first if they exist.
6. Worked example — pool plus per-country override, with a preview
A messaging servicems_ops01 carries sender_pool_id: pool_default (a sticky pool of US numbers) and a country_sender_pools map { "GB": "pool_gb" } (pool_gb is geomatch, holding two UK numbers plus one DE number). A send to a GB recipient comes in with no explicit sender field:
GB entry beats the service default pool), pool_gb resolves via geomatch, and the pick lands on one of the UK numbers — the same chain walked end-to-end on Sender resolution; here the focus is the pool step.
Precheck the pick without sending:
See also
- Sender resolution — the full outbound precedence chain this pool plugs into, including
sender_pool_idvsfromand the 3-step fallback - Choosing a sender construct — decide between pool, messaging service, and inline sender
- Messaging services — the defaults + per-country override map that target pools
- Sender pools guide — create pools, preview picks, read health end-to-end
- Sender Pools API — endpoint reference and error surfaces
- Sender warming and reputation — the reputation accrual kernel the rotation scheduler consumes
- Fix an empty sender pool — resolve a
SENDER_POOL_EMPTY