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

# Fix an empty sender pool (`SENDER_POOL_EMPTY`)

> A routed sender pool resolved fine but has zero members, so the send is rejected with 422 before anything is queued. Here is how to confirm, fix, and safely retry it.

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

| State | What the pool resolves to | What a send does |
| - | - | - |
| **empty** | The pool row exists, `sender_dids` is zero-length | Rejected with `SENDER_POOL_EMPTY` before anything is queued |
| **resolved** | At least one eligible member for the recipient | The pool picks a sender per its strategy and the send queues |
| **failed-to-resolve** | The pool exists but nothing matches this recipient | A sibling code fires — see the table below |

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:

| Code | HTTP | What fired | Clears when you |
| - | - | - | - |
| `SENDER_POOL_EMPTY` | 422 | The pool **exists** but has no members | Add a member to that pool, or pass a different `sender_pool_id` |
| [`SENDER_POOL_NOT_FOUND`](/troubleshooting/sender-resolution-errors) | 404 | The pool id names **no pool at all** — a wrong or deleted id | Verify the id, or re-create the pool |
| [`NO_SENDER_CONFIGURED`](/troubleshooting/sender-resolution-errors) | 422 | No pool was named, and your organisation has **no sender anywhere** — no default sender, no active phone number | Register a sender first |

`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](/guides/sms-services) 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](/concepts/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](/guides/sender-pools) 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](/channels/instagram) 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

| Code | Clears when you |
| - | - |
| `SENDER_POOL_EMPTY` | Add a member to the named pool, or pass a different `sender_pool_id` (fresh idempotency key if the body changed) |
| `SENDER_POOL_NOT_FOUND` | Verify the pool id (or re-create the pool) |
| `NO_SENDER_CONFIGURED` | Register a sender at organisation level first |
| `IDEMPOTENCY_KEY_REUSED` (after your fix) | Resubmit with a fresh key when the corrected body differs |

## Example envelope

```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": "+14155550100",
    "sender_pool_id": "pool_emptypool",
    "body": "Order shipped"
  }'
```

returns HTTP 422:

```json theme={null}
{
  "error": {
    "code": "SENDER_POOL_EMPTY",
    "status": 422,
    "message": "Sender pool pool_emptypool has no DIDs configured"
  }
}
```

## Related references

* [Sender pools](/guides/sender-pools) — create pools, add members, and
  understand selection strategies.
* [Sender resolution](/concepts/sender-resolution) — where the pool pick
  sits in the full precedence chain.
* [Idempotency and safe retries](/concepts/idempotency-and-safe-retries)
  — the key-vs-body binding semantics the retry section above applies.
* [Troubleshoot sender resolution and pool errors](/troubleshooting/sender-resolution-errors)
  — the three sibling codes (`SENDER_REQUIRED`, `SENDER_POOL_NOT_FOUND`,
  `NO_SENDER_CONFIGURED`).
* [FAQ: Why did my send return `422 SENDER_POOL_EMPTY`?](/reference/faq)
  — the one-line version of this page.
* [Error codes](/reference/error-codes) — the full sender error table.


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