Skip to main content

Dil notu

Çeviri mevcut değilse İngilizce içerik yedek olarak gösterilir. Hata kodlarını, API yollarını ve kod bloklarını değiştirmeyin.

Fix an empty sender pool (SENDER_POOL_EMPTY)

SENDER_POOL_EMPTY is a deterministic, pre-send rejection: your request named a valid pool, Orbit looked up the pool, and found it has no members to draw a sender from. The send never reaches a provider, so nothing needs a carrier-side reconciliation — add a member to the pool (or route through a different pool) and send again.

Symptoms

  • The send request returns HTTP 422 with code SENDER_POOL_EMPTY, and the response names the pool that was empty.
  • The dashboard Delivery Log shows no row for the message — the send was rejected before queueing, so there is nothing to deliver.
  • The pool’s detail page prompts you to add a DID, and the senders list for the pool is empty.
An identical 422 also comes back from the pool preview endpoint (GET /api/v1/messaging/sender-pools/{id}/preview) when the pool has no members, so the symptom surfaces before you send real traffic if you preview first.

The sender-pool state machine

An empty pool sits between two healthier states, and a third failure state shares its 422 status. Tracking which state you are in tells you whether a fix has landed: The pool preview endpoint exercises that resolution step read-only, so you can walk the state machine without sending traffic — the empty → resolved transition shows up there the moment a member lands. The neighbouring codes split the failed-to-resolve state by where the break sits, and each one has its own clear action: SENDER_POOL_EMPTY is pool-level state (a good pointer, an empty list), SENDER_POOL_NOT_FOUND is the pointer itself, and NO_SENDER_CONFIGURED is organisation-level state.

Causes

The pool row exists and resolves — the failure is membership:
  • The pool’s sender_dids list has no entries. Two ways a pool reaches that state: it never had members (created with an empty list and never filled, or the last member was removed), or the send carried a narrower recipient filter and none of the pool’s members were eligible, leaving zero candidates for that recipient.
  • A messaging service, campaign, or sender routing rule still points at the pool, so every send routed through it fails with the same code.

Pool membership mechanics

A pool member can be an E.164 DID, a numeric short code, or an alphanumeric sender ID — owned by your organisation:
  • E.164 DID — listed by GET /api/v1/numbers; check the inventory before attributing the empty-pool rejection to a missing row.
  • Numeric short code or alphanumeric sender ID — set via the API (PATCH /api/v1/messaging/sender-pools/{id} with sender_dids); the dashboard’s pool editor carries them over when you edit there.
One membership constraint matters upstream of the 422: a sender can belong to exactly one pool, so moving a sender between pools is a delete-then-add, and a “vanishing” member is usually that move rather than a data problem. The 422 + idempotency interaction below applies whatever the membership form is.

The in-flight retry semantics

SENDER_POOL_EMPTY is rejected before any pick is made, so no selection state was written for the recipient — a retries-after-fix sequence moves from correct-by-construction to explicitly defined:
  • The idempotency key survives the pool swap. The key scopes to the request, not to the pool id inside it — when you fix membership (or route through another pool) and resubmit the same key with the same body, Orbit returns the cached 422 instead of a second send.
  • The body must be byte-identical. If your fix changes the body — for example a different sender_pool_id value — the same key resubmits to 409 IDEMPOTENCY_KEY_REUSED, not to a send. Generate a fresh key for the corrected request, or leave the key unset (neither the 422 nor the 409 consumes the rejection).
  • Nothing wrote a cache entry to clean up. Because the rejection happens before queueing, the idempotency cache holds either nothing (the plain rejection) or the 422/409 itself — both expire on the key’s 24-hour window. There is no mid-batch selection state to unwind; the idempotency and safe retries concept page covers the key-vs-body binding in full.
The recovery-safe shape: fix the pool (or the route) → resubmit the same body under the same key (cached 422 → new outcome) or a fresh key (body changed) → confirm against the preview endpoint or the Delivery Log. Your queue-derived key stays stable across the pool fix when the payload is untouched.

Gate it before launch: preview as a diagnostic

The preview endpoint rejects with SENDER_POOL_EMPTY the same way a send does, and stays read-only — it never advances a round-robin counter or writes a sticky assignment, so it costs nothing to run as a pre-launch gate.
  • Run GET /api/v1/messaging/sender-pools/{id}/preview?recipient=<E.164> on a synthetic recipient after provisioning or editing a pool; expect a sender, not the 422, before you wire the pool into a campaign or messaging service.
  • Combine with GET /api/v1/numbers so the pre-launch check both resolves and owns its sender before traffic flows.
  • When a recipient filter narrows the pool to zero candidates, preview the actual recipient you will send to — the failure mode is narrower than the empty list the pool row itself reports.
See the Sender Pools guide for the full preview flow and the console member-health warning this endpoint avoids.

Route elsewhere: pass a sender_pool_id, or repoint the wiring

When the right move is to sidestep the empty pool rather than fix it:
  • One-off divergence — pass a different sender_pool_id on the send request itself (generate a fresh idempotency key, per the retry semantics above). Use this when only certain traffic should route away.
  • Wiring repoint — move the messaging service’s default pool, the campaign, or the routing rule to a non-empty pool when everything routed through the empty pool should move. Use this when the pool itself is being retired or reworked.
Either way, the 422 loop stops when the pointer reaches a member — decide whether the empty pool should stay wired at all.

Client hints: branch on the confirmed channel and sender

Once a sender is in the pool, the fuller client-hint model covered on the Instagram channel page applies here too: pick the outbound sender on metadata.channel and the source message.from of the inbound event that started the thread, rather than hardcoding a pool member. For a pool-backed thread the inbound event narrows the candidates to a remembered or geo-matched pick.

Composite recovery

Example envelope

returns HTTP 422: