Skip to main content

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 with sender_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_id wins over a bare from, 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, or geomatch. The strategy is the pool’s own property — change it with PATCH and 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.
Between a send request and the wire, the pool sits after the precedence chain resolves 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 sticky and geomatch’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 geomatch match 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: true forces geomatch on the pool regardless of the pool’s own stored sticky / 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:
  1. 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.
  2. The rotation scheduler. A 6-hour autopilot reconciles pool membership against health: a member that dropped to poor or critical is quarantined out of the set and, when a warmed non-degraded replacement exists, substituted one-for-one.
  3. 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.
  4. 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.
The distinguishable pair to keep straight: warming accrues reputation on a single sender over time (that page); the rotation consumes already-accrued reputation to move members in and out of a pool (this page). At send time, a send-time gate on inventory-bound pools also re-picks away from a quarantined first pick when a healthy member remains — sending from a degraded number only when no healthy option exists, and never failing the send on reputation alone.

4. Failure classes: empty pool and no healthy members

The one hard failure a pool can produce is SENDER_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_dids is empty (no members ever added, or all members removed). Sends through the pool fail fast with 422 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.
What the platform does next: nothing implicit. An empty pool does not silently fall through to the org default or platform fallback chain — the send rejects with the 422 so you see the mis-route instead of a silent substitution into the 3-step fallback. Restock the pool (add at least one DID), and retry; the same send then picks a member fine. The full symptom-cause-fix walkthrough lives in Fix an empty sender pool. “No healthy members” is not an empty pool and does not raise 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

  • CreatePOST /api/v1/messaging/sender-pools with a label, 1–50 member sender ids, and an optional strategy (default sticky). Owner/admin only.
  • Add/remove membersPATCH replaces 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).
  • PreviewGET .../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 drainGET .../sender-pools/{id}/health rolls up per-member tiers and counts (at_risk, fully_degraded); the dashboard rotation-alerts panel renders the same roll-up.
  • DeleteDELETE .../sender-pools/{id} removes the pool and its sticky-assignment rows. New sends referencing the id start failing 404 SENDER_POOL_NOT_FOUND from 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 service ms_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:
The per-country map fires first (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:
The preview never commits: the same recipient on a real send returns the same member only because the pick is deterministic within the GB-matched subset, not because the preview wrote anything.

See also