Skip to main content

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; the endpoint level field tables are in Phone Numbers — 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: 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: The transitions:
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 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