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

# Bulk number-order recovery: decode failed lines to the remediation

> Troubleshoot a failed or partial bulk DID purchase — read the aggregate status and per-line error codes from GET /numbers/orders and GET /numbers/orders/:id, then route each failure class to its fix: re-search inventory, narrow capability, complete a compliance profile, or top up balance.

# Bulk order recovery: the troubleshooting runbook

A bulk DID purchase fails at the line level, not the order level — `POST /numbers/buy-bulk` returns `200` even when every row fails, because the status code reports that the batch was processed, not that it delivered. This runbook is the recovery half of the ledger: [Bulk order history](/guides/numbers-bulk-orders-history) tells you where the order record lives and what org-scoping it carries; this page tells you what to DO with a `partial` or `failed` status once you find one.

The loop this page teaches: read the order, decode each failed line's error code, apply the play for that code, and re-issue only the failed lines as a fresh batch. Failed lines are never charged, so the loop is always safe to run.

## 1. The failure model: per-line errors, aggregate status

Every bulk order resolves to one aggregate `status` plus a per-line breakdown:

* **`delivered`** — every requested line succeeded. Nothing to recover.
* **`partial`** — some lines delivered, some failed. Recover only the failed lines; the delivered ones are already active and paid for (`debited_cents` covers exactly the success subset).
* **`failed`** — no line delivered; nothing was charged (`debited_cents: 0`). The whole batch is recoverable, or one shared cause (balance, compliance profile) broke every line at once.

The decision that separates a retry from a re-plan:

* **Retry when the cause is transient or inventory-side** — `NUMBER_NO_LONGER_AVAILABLE`, `NUMBER_ALREADY_TAKEN`. Replace the failed E.164s with fresh inventory and resubmit. Same request shape, new numbers.
* **Re-search when the cause is a request constraint** — `CHANNEL_UNAVAILABLE` (a capability the carrier row never had) or a region with no coverage. Change what you ask for — narrow `capabilities`, drop the area code, switch country — before resubmitting. Retrying the same constraint against the same inventory burns your 2-requests-per-minute create budget on a guaranteed failure.
* **Fix the gate first when the cause is account-side** — `INSUFFICIENT_BALANCE` or a missing/unapproved compliance profile. No amount of inventory reshuffling helps until the wallet is funded or the profile is approved.

## 2. Reading the two order APIs

Both reads need an API key with the `numbers:read` scope; neither mutates anything.

**`GET /numbers/orders`** — the list endpoint. Returns recent orders as summaries, newest first: `order_id`, aggregate `status`, `requested_count` / `delivered_count` / `failed_count`, `total_cost_cents`, `debited_cents`, `created_at`. Use it to find orders you did not capture at create time, or to sweep for any `partial`/`failed` status in a nightly reconciliation job.

**`GET /numbers/orders/:id`** — the detail endpoint. Returns one order with the full per-line `lines` array: each line carries `phone_number`, `line_status` (`delivered` or `failed`), and the success fields (`country_code`, `capabilities`, `monthly_cost_cents`) or the failure `error` object (`code`, `message`). This is the decode surface — the `error.code` on each failed line is what routes you to a play in §4.

Orders are org-scoped: an unknown or not-owned id returns `404` — a missing id never leaks that the order exists in another org.

## 3. Error-code decoder

Per-line failure codes fall into four classes. Match the code to its class, then play the class:

| Code | Class | Play |
| - | - | - |
| `NUMBER_NO_LONGER_AVAILABLE` | Inventory turnover — another buyer claimed the number between your search and checkout | **Re-search inventory** and resubmit with fresh rows (§4, Play 1) |
| `NUMBER_ALREADY_TAKEN` | Your org already owns the number — the per-number mutex caught a re-submit | **Drop the line**; you already own it — no re-search needed |
| `CHANNEL_UNAVAILABLE` | Capability mismatch — the requested `sms`/`voice`/`whatsapp`/`rcs` set can't be provisioned on that row | **Narrow the capability request** to the carrier's advertised set (§4, Play 2) |
| `COMPLIANCE_PROFILE_REQUIRED` / `COMPLIANCE_PROFILE_NOT_APPROVED` | Regulatory hold — a registration-required country has no approved profile | **Complete and approve the profile**, then pass its id on the batch (§4, Play 3) |
| `INSUFFICIENT_BALANCE` | Wallet gate — fails the WHOLE batch with `402` before any carrier call | **Top up the wallet**; the error body names the required cents |
| Carrier-hold codes (region or carrier-policy rejections, surfaced in the `error.message`) | Region/carrier coverage | **Exclude the region or switch country**, or move to the bulk-reserve path (§4, Play 4) |

A `partial` order can mix classes — decode each failed line independently. When an unfamiliar code arrives, class it with the same rule of thumb: inventory-side → re-search; constraint-side → narrow the request; account-side → fix the gate; region-side → exclude or switch.

## 4. Remediation plays per class

### Play 1 — Inventory turnover (`NUMBER_NO_LONGER_AVAILABLE`)

Inventory ages in minutes on hot area codes; the number you searched may belong to another buyer by checkout. The fix is a fresh search, not a blind retry:

1. Re-run `GET /numbers/available` with the same filters — the search criteria in [Buy and provision numbers §4](/guides/buy-numbers#4-bulk-provision-up-to-50-numbers).
2. Replace each failed E.164 with a fresh row from the new results.
3. Resubmit a batch containing **only the replacements**. Resubmitting a delivered line fails that line fast with `NUMBER_ALREADY_TAKEN` (the per-number mutex) and is never double-billed — but it wastes one of your 2-creates-per-minute.

### Play 2 — Capability mismatch (`CHANNEL_UNAVAILABLE`)

The requested capability set didn't intersect the carrier's advertised set for that row. Two ways out:

* **Narrow the request**: pass exactly the capability subset the inventory row advertised (`GET /numbers/available` returns each row's `capabilities`). An explicit request on a voice-only DID that asks for `sms` writes the mismatch; pass `["voice"]` instead — see the `capabilities` field in [Buy and provision numbers §4](/guides/buy-numbers#4-bulk-provision-up-to-50-numbers).
* **Switch number type**: a toll-free number's SMS is send-only. If you need two-way SMS, buy a local or mobile DID instead of forcing the capability onto a type that can't carry it.

### Play 3 — Compliance gate (`COMPLIANCE_PROFILE_REQUIRED`, `COMPLIANCE_PROFILE_NOT_APPROVED`)

A registration-required country rejected the batch because no approved profile was attached. Approval happens outside the order flow:

1. Complete the profile per [Regulatory Preview](/numbers/regulatory-preview).
2. Wait for approval — a `COMPLIANCE_PROFILE_NOT_APPROVED` line retries into the same failure until the profile flips.
3. Resubmit the batch with `compliance_profile_id` at batch level (one profile per order).

Porting a batch instead of buying one? The same gate exists there — the [batch port-in walkthrough](/guides/port-numbers-batch-walkthrough) runs the full preflight → LOA → dispatch sequence for ported inventory.

### Play 4 — Region/carrier coverage (carrier-hold codes)

When the `error.message` names a region or carrier hold rather than inventory turnover:

* **Exclude the region**: drop the area code or locality from your search filters and re-search. A US 50-line batch blocked on one metro splits cleanly.
* **Switch country**: if your use case tolerates it, an adjacent country with the same capability set often clears a coverage hold.
* **Move to the bulk-reserve path**: for blocks over 50, or when you want the platform to hold matching DIDs while your batch-level decisions settle, use [Bulk Reserve](/guides/bulk-reserve) — hold up to 1000 DIDs matching a country/capability filter for 15 minutes, then finalize or cancel. The reserve-then-finalize shape sidesteps the search-to-checkout race that causes `NUMBER_NO_LONGER_AVAILABLE` on the direct buy path.

## 5. Polling cadence and the rate-limit asymmetry

The rate-limit asymmetry is the whole polling design: `POST /numbers/buy-bulk` is limited to **2 requests per minute per tenant** (one call fans out to up to 50 carrier calls), but the order-read endpoints carry **no such ceiling** — poll `GET /numbers/orders/:id` on whatever cadence your reconciliation needs; the create-path limit never gates reading.

Practical cadence for a remediation loop:

1. Issue the batch (create-path budget: 2/min).
2. Poll the detail endpoint every few seconds until `status` is no longer moving — bulk purchases resolve in one request, so one read is usually enough.
3. Decode the failed lines, run the play, resubmit (second create in the same minute-window).
4. A nightly sweep over `GET /numbers/orders` (list) catches anything nobody polled live.

The trap to avoid: a poll-and-retry loop that fires a fresh `buy-bulk` per poll interval. The create budget is the scarce resource; reads are free.

## 6. Worked remediation loop

The scenario: a two-line US batch comes back `partial`. Line 1 delivered; line 2 failed with `NUMBER_NO_LONGER_AVAILABLE`.

**Step 1 — read the order.** `GET /numbers/orders/numOrder_01J9Z8ABCDEF` returns:

```json theme={null}
{
  "data": {
    "order_id": "numOrder_01J9Z8ABCDEF",
    "status": "partial",
    "requested_count": 2,
    "delivered_count": 1,
    "failed_count": 1,
    "total_cost_cents": 300,
    "debited_cents": 150,
    "created_at": "2026-08-24T12:00:00.000Z",
    "lines": [
      {
        "phone_number": "+14155550100",
        "line_status": "delivered",
        "country_code": "US",
        "capabilities": ["sms", "voice"],
        "monthly_cost_cents": 150
      },
      {
        "phone_number": "+14155550101",
        "line_status": "failed",
        "error": { "code": "NUMBER_NO_LONGER_AVAILABLE", "message": "Number no longer available" }
      }
    ]
  }
}
```

**Step 2 — decode.** `NUMBER_NO_LONGER_AVAILABLE` is inventory turnover → Play 1. `debited_cents: 150` of the `total_cost_cents: 300` confirms only the delivered line was charged.

**Step 3 — re-search.** `GET /numbers/available` with the same filters returns a fresh candidate, say `+14155550220`.

**Step 4 — re-issue just the failed line.** Submit a one-line batch containing only the replacement:

```bash theme={null}
curl -X POST "https://api.orbit.devotel.io/api/v1/numbers/buy-bulk" \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{"items": [{ "phone_number": "+14155550220" }]}'
```

The resubmitted batch carries no bundle-level idempotency key — replace failed lines only, and let the per-number mutex guard anything you re-send. The original order record is immutable; the new batch creates its own `order_id`, and the pair reconciles against usage exports via [CDR export & billing reconciliation](/billing/cdr-export-billing-reconciliation).

## 7. Recovery in the dashboard

The same ledger renders under **Numbers → Buy → Orders**. Open an order row to expand the per-line breakdown; each failed line shows its carrier error inline, so the decode step happens in the console without an API read. Re-search and re-issue from **Numbers → Buy** — the console's inventory search is the same `GET /numbers/available` behind the API play.

## See Also

* [Bulk order history](/guides/numbers-bulk-orders-history) — the ledger surface this runbook decodes: both read endpoints, org-scoping, and the reconcile-against-usage pattern.
* [Buy and provision numbers](/guides/buy-numbers) — the search → buy → wire loop, with the `buy-bulk` request shape in §4.
* [Bulk Reserve](/guides/bulk-reserve) — hold up to 1000 DIDs, then finalize or cancel — the path that skips the search-to-checkout race.
* [Batch port-in walkthrough](/guides/port-numbers-batch-walkthrough) — the staged-shipment recovery discipline when the numbers come from porting, not purchase.
* [Inbound health](/guides/numbers-inbound-resolution-health) — after recovered lines land, repair any missing routing before traffic starts.


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