> ## 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 Reserve model: the reservation state machine

> How a bulk reservation thinks — the draft → held → finalized/cancelled state machine, the 15-minute no-charge hold, both-feeds cheapest-pick scan semantics, the US area-code tile map input, and exactly when the wallet preflight fires.

# Bulk Reserve model

Bulk Reserve is one of three number-purchase surfaces in Orbit, and the only
one with a *reservation* layer. You describe the shape of a block (country,
number type, maybe an area code, a quantity up to 1000), Orbit scans both
upstream inventory feeds and picks the cheapest matching numbers, and the
block is held for 15 minutes without charging you. You then finalize it
(purchase + bill + route) or cancel it (release). This page is the
concept-level model behind those operations — the state machine, the
hold/expiry friction, and where the wallet is touched. The step-by-step
console walkthrough lives in [Bulk Reserve in the
console](/guides/bulk-reserve); the endpoint level field tables are in [Phone
Numbers — Bulk Reserve](/numbers/overview#bulk-reserve).

## The three purchase surfaces

All three DIDs-acquisition surfaces land on the same purchase machinery; they
differ in *how the numbers get chosen*:

| Surface                                      | You provide                      | Cap                        | Who picks the numbers                                      |
| -------------------------------------------- | -------------------------------- | -------------------------- | ---------------------------------------------------------- |
| **Numbers → Buy**                            | Nothing — you browse             | 1 at a time                | You pick the exact E.164 from search results               |
| **Numbers → Buy** (select list / `buy-bulk`) | A pre-built list of exact E.164s | Up to 50 per order         | You name every number; Orbit buys that list                |
| **Numbers → Buy → Bulk** (Bulk Reserve)      | A filter                         | Up to 1000 per reservation | Orbit picks the cheapest match across both inventory feeds |

The distinction is **filter-vs-exact-pick**: single Buy and `buy-bulk` are
exact-pick (you name the E.164 yourself), Bulk Reserve is filter-first (you
name the shape, Orbit supplies the numbers). Both feeds are treated as a
single pool by the reserve path unless you pin one provider.

## The reservation state machine

A reservation is a row with a closed status vocabulary. The service-side
values are `reserving`, `ready`, `partial`, `failed`, `finalizing`,
`finalized`, `cancelled`, and `expired`; reading them as a state machine,
they collapse into three behavioral clusters:

| Cluster         | Statuses                                      | What defines the cluster                                                                                                                                         |
| --------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Draft / holding | `reserving`, `ready`, `partial`               | The block is picked and held. The wallet has not been touched. Inventory is still external — another tenant can sweep the same row at finalize time.             |
| Transient       | `finalizing`                                  | A finalize call has acquired the row lock and is purchasing per item. A second finalize arrives as 409; the row reverts to `ready` on a preflight-level failure. |
| Settled         | `finalized`, `cancelled`, `expired`, `failed` | Terminal. `finalized` and `cancelled` and `expired` reject any further finalize or cancel with 409. `failed` is the all-items-failed outcome of a finalize.      |

The transitions:

```
create              → ready    (full fill)    or  partial   (inventory ran short)   or  failed (zero match)
finalize (held)     → finalizing → finalized  (all items succeed)
finalize (held)     → finalizing → partial    (some items succeed, some fail)
finalize (held)     → finalizing → failed     (every item fails)
finalize (held)     → finalizing → ready      (revert on a preflight-level reject like an insufficient-balance throw)
cancel (held)       → cancelled               (items released; no carrier call)
read after expiry   → expired                 (read-time, inline — no sweep required)
```

The same transitions drive the console page (held → settled banner) and the
JSON you read off `GET /numbers/bulk-reserve/:id`.

Two moves you can never make: you cannot re-hold a settled reservation, and
you cannot cancel a finalized one. Release after finalize is per number via
`DELETE /numbers/:id`, because the items are now owned, billed DIDs.

## Inventory scan semantics

Creating a reservation issues one search against the union of both upstream
inventory feeds (DIDWW + Telnyx, joined by the same index the single-Buy
page reads) and picks the **cheapest matching N** across that union. The
picks are sorted by monthly cost ascending with E.164 as the tie-break, so
two identical reserve requests select the same rows.

Three properties to internalize:

* **Both feeds, or one.** Pinning `provider: "didww"` or
  `provider: "telnyx"` restricts the scan to that feed; unpinned scans both.
  Pinning matters when you need a particular carrier's regulatory posture
  (e.g. DIDWW-only regions) even at higher cost.
* **Partial fill is reported, not thrown.** If the filter matched fewer
  numbers than `quantity`, the reservation reports `status: "partial"` with
  the count it could hold. You finalize the smaller block or cancel and
  widen the filter.
* **Headroom in the search.** The per-provider fan-out asks for up to 1.5×
  your requested quantity (capped at 500 rows) so the picks survive carriers
  turning stock between your search and your finalize.

## The US area-code tile map

For US + local reservations the console filter grows an area-code picker
backed by a geographic tile map. Every state (plus DC and the US territories
PR, VI, GU, AS, MP) renders as a uniform-size tile; inventory density is
encoded by color, so the map is not a choropleth and screen readers reach
each tile separately. Clicking a tile writes the corresponding `area_code`
into the filter; a multi-code state opens a popover. Tiles without current
stock are dimmed and not clickable — the map mirrors what the upstream
feeds actually group into each region.

The tile grid and the free-text NPA box both write the same `area_code`
field, so API callers who don't render the console can send the raw 3-digit
code directly. Multi-area-code blocks run one reservation per area code;
the [worked example](/guides/bulk-reserve) in the guide does three.

## When the wallet preflight fires

The wallet preflight runs **only at finalize**, never at reserve. Creating
a reservation never debits — the picking-and-persisting step charges
nothing, so a tenant that discards the hold costs nothing. At finalize the
summed per-item cost is checked against the balance before any per-item
purchase is attempted; insufficient balance stops the finalize with the
required and available amounts named, **before** a single carrier call
leaves. The reservation stays in `ready`, so you can top up and retry
within the 15-minute window.

This is the only wallet touch point in the reservation lifecycle. Every
other state move is free.

## Hold expiry and re-hold

The hold lasts **15 minutes** from creation (`expires_at` stamped at
create). It is server-side — closing the tab does not release it — and it
releases read-time: any `GET /numbers/bulk-reserve/:id` after `expires_at`
flips the row to `expired` and its items to `released`, without waiting on
a sweep job. Once expired the row is settled; no finalize or cancel path
re-opens it.

Re-holding is not a first-class operation. If a hold lapses, create a new
reservation — inventory is re-picked at creation, so the new block may not
return the same numbers the old one held.

## Finalize failure modes and their recoverability

Finalize can fail at three layers, and each recovers differently:

* **Wallet preflight reject.** The summed per-item cost exceeds the
  balance. No carrier call, no item moved, the reservation stays `ready`.
  Recoverable: top up and re-finalize within the hold.
* **Per-item failures mid-purchase.** Some items purchase and others are
  rejected upstream (compliance gate, carrier-side race). Succeeded items
  are purchased, billed, and given an inbound route; failed items carry
  their error code and message on the row. The reservation closes as
  `partial` with the succeeded/failed counts. Recoverable: point the
  succeeded items and either re-create the failed slice as a new
  reservation or accept the partial block. No partial debit — failed items
  are never billed.
* **Every item failed.** The finalize flips the reservation to `failed`.
  Recoverable only by creating a new reservation; the failed row is
  settled.

A per-item **regulatory-compliance gate** applies before purchase just
like the single-pick path, so attaching a `compliance_profile_id` on the
reservation's filter is the way to pre-clear a block in a regulated
country.

## See also

* [Bulk Reserve in the console](/guides/bulk-reserve) — the step-by-step
  walkthrough: filter panel, tile map, held page, finalize, cancel, and the
  worked 3-area-code example.
* [Phone Numbers — Bulk Reserve](/numbers/overview#bulk-reserve) — the
  endpoint level: request fields, response shapes, and the 1/minute rate
  limit on create.
* [Number status map](/concepts/number-lifecycle) — the six-state
  vocabulary finalized numbers join once they are owned.
* [Buy and provision numbers](/guides/buy-numbers) — the exact-pick
  surfaces, up to 50 per select-list order.
