Skip to main content

Phone Numbers

Orbit provides virtual phone numbers across a broad, provider-driven country footprint for SMS, voice, and multi-channel messaging. Search available numbers, purchase instantly, and configure routing — all through the API or dashboard. Per-country and per-type availability is provider-driven and changes over time, so it is not hardcoded here. Query the live source of truth instead:
  • GET /api/v1/numbers/available/countries — countries that currently have searchable inventory.
  • GET /api/v1/numbers/country-capabilities — per-country line_type × capability counts (local/mobile/toll-free, SMS/voice), including the authoritative two_way_sms count. Note that toll-free SMS is send-only (toll-free is never two-way), which is why toll-free SMS is surfaced separately from two_way_sms.
This page covers search, purchase, routing, and porting. The rest of the number-management surface is documented on dedicated pages:

Search Available Numbers

Response

The matching numbers are nested under data.numbersdata is an object, not an array. Prices are decimal strings: monthly_cost is the recurring fee and setup_cost the one-time activation fee, both denominated in the currency field (currently USD). setup_cost is always present — it is "0.00" when there is no setup charge.
Fields prefixed with an underscore (e.g. _provider) are internal and may change without notice — do not depend on them.

Purchase a Number

Bulk Purchase

Buy a known list of numbers in a single call with POST /api/v1/numbers/buy-bulk. Use this when you already have the exact E.164 numbers (for example from GET /numbers/available) and want to provision up to 50 of them at once. The wallet is checked up-front against the sum of the per-number costs. Per-row carrier failures are reported in failed[] and never cause partial-batch debits — either every charged number is debited, or none is. The endpoint is rate-limited at 2 requests/minute per tenant because each call can fan out to 50 upstream provider calls.

Request body

Response

Returns 200 OK even on partial success — inspect succeeded[] and failed[] to render per-row results without parsing 4xx bodies. (A batch where every item fails is still 200; check succeeded.length to decide UI state.)
total_cost_cents is the summed cost of all requested items; debited_cents is the amount actually charged for the items in succeeded[].

Bulk Reserve

When you need to provision a contiguous block of DIDs but don’t yet have the specific numbers, use the bulk-reservation lifecycle. You reserve up to 1000 DIDs that match a country / area-code / region filter, the platform holds them for 15 minutes, then you finalize (purchase + bill + create inbound routes) or cancel (release the hold). This closes the gap where buy-bulk requires a pre-built list of E.164s.

1. Reserve

POST /api/v1/numbers/bulk-reserve searches provider inventory (DIDWW + Telnyx, picking the cheapest N across the union unless provider is pinned) and places a 15-minute hold. Rate-limited at 1 request/minute per tenant.
Returns 201 Created. status is ready when the full requested quantity was found, or partial when fewer matched. The reservation expires at expires_at (creation + 15 minutes).

2. Inspect

GET /api/v1/numbers/bulk-reserve/:id returns the current reservation state and items. The row is inline-expired on read once expires_at has passed, so a held-but-never-finalized reservation reports as expired without a separate sweep.

3. Finalize

POST /api/v1/numbers/bulk-reserve/:id/finalize purchases, bills, and creates inbound routes for every reserved item. It runs the same wallet preflight, per-item compliance gate, and per-item audit as buy-bulk. Per-item failures are reported in the response counts; no partial-batch debit occurs.

4. Cancel

POST /api/v1/numbers/bulk-reserve/:id/cancel releases every reserved item back to the carrier inventory pool. No upstream carrier call is made — a reservation never placed an order. Returns 409 if the reservation has already been finalized, cancelled, or expired.
A reservation that is never finalized or cancelled is released automatically when its 15-minute hold expires.

Configure Routing

Update the number record

Update a number’s settings — including a generic inbound webhook and SMS forwarding — with PUT /api/v1/numbers/:id:
The update body accepts only these fields: channel_config, status, label, capabilities, forwarding_number, webhook_url, sms_forwarding, compliance_profile_id, messaging_mps_cap, monthly_spend_cap_cents, and tags. Unknown fields are rejected with a VALIDATION_ERROR (422). Use webhook_url for a single generic inbound webhook and sms_forwarding (E.164) to forward inbound SMS to another number. Set messaging_mps_cap to an integer between 1 and 1000 to cap this number’s outbound messages-per-second rate, or null to clear the cap (uncapped). Set monthly_spend_cap_cents to an integer between 1 and 100,000,000 cents (up to $1,000,000) to cap this number’s spend per calendar month, or null to clear the cap (uncapped).

Route inbound calls or SMS to an agent or queue

To send inbound calls or messages to an AI agent, IVR, queue, ring group, voicemail, or other destination, use the dedicated inbound-routing surface keyed by the number in E.164 form — agent_id is not a field on the number record:
The voice type is one of agent, softphone_user, softphone_register, ivr, voicemail, queue, sip_forward, transfer, conference, decline, ring_group, shared_line, dispatch_to_voicemail_box, or webhook; each type has its own config shape. The last three are:
  • shared_line — fork-rings every member device of a shared line so any member can answer. config is { sharedLineId, ringTimeoutSec? }, where sharedLineId references a shared line created via /voice/shared-lines and ringTimeoutSec is 5120 (optional).
  • dispatch_to_voicemail_box — drops the caller into a shared department mailbox. config is { boxId, greetingUrl?, maxDurationSec? }, where boxId references an existing voicemail box in this organization and maxDurationSec is 5900 (optional).
  • webhook — fetches call control verbs from your own HTTPS endpoint on every inbound call. config takes exactly one of { url } (an HTTPS URL) or { staticVerbs } (an inline sequence of 1–50 verbs served without a fetch), plus optional method (POST default, or GET), timeoutSec (110, default 5), and fallbackBehavior (safe-default default, voicemail, or decline).
Inbound SMS termination is configured independently via sms_route_type (webhook, agent, auto_reply, or disabled) and sms_route_config. Retrieve the current route with GET /api/v1/numbers/:phoneNumber/routing.

Number Types

Pass the type value to the search endpoint, e.g. ?type=toll_free. Short codes are not available for search or purchase through the API. Which countries and types are actually available is provider-driven — rather than a fixed matrix, query GET /api/v1/numbers/available/countries (countries with current inventory) and GET /api/v1/numbers/country-capabilities (per-country counts by line type and capability, including the authoritative two_way_sms count) for live availability.

Number Capabilities

Capabilities are expressed through two distinct enums — the search filter and the purchase field are not interchangeable, and passing a value from the wrong set returns 422:
  • Search filter — the capabilities query param on GET /api/v1/numbers/available accepts a comma-separated subset of sms, mms, voice, fax. whatsapp/rcs are not valid here and are rejected with 422.
  • Purchase field — the capabilities array on POST /api/v1/numbers/purchase accepts sms, voice, whatsapp, rcs. Use this enum (not the search one) to request over-the-top channels at provisioning time; mms/fax are not purchase-time toggles.

Number Porting

Bring your existing numbers to Orbit. Port-in is a four-step flow — check eligibility → attach a signed LoA → submit → track. Don’t submit blind: the pre-flight check tells you whether the number is portable and returns the carrier’s estimated completion date, so you don’t burn business days on a doomed order. You can run the whole flow from the dashboard under Numbers > Port a Number, or via the API as below.

1. Check portability (pre-flight)

Run a read-only eligibility check before you submit. This calls the resolved carrier (Telnyx for US/CA numbers, DIDWW for everything else) and returns the verdict synchronously — no order is created and no credits are consumed. Rate-limited to 20 requests/minute per tenant.
There is no flat porting SLA — completion time depends on the country and losing carrier (carrier-published ranges run from roughly 5 business days to 15+ business days, with most non-US geographies in the 4–10 business-day band). Use estimated_foc_date from this check and the live timeline endpoint for the authoritative estimate rather than a hardcoded figure. For countries where check_status is check-not-supported, the carrier confirms eligibility after submission (DIDWW does so within ~2 business days).

2. Attach a signed Letter of Authorization (LoA)

Carriers require a signed LoA to release the numbers. Upload the LoA PDF via POST /api/v1/files/upload and pass the returned GCS-signed URL as loaFileUrl when you submit (step 3). loaFileUrl must be a Devotel-issued storage.googleapis.com signed URL — a caller-supplied or arbitrary URL is rejected with LOA_URL_INVALID_ORIGIN (422). Alternatively, submit first in manual mode (omit loaFileUrl), then attach and sign the LoA against the created request:

3. Submit the port-in request

4. Track the port

Port Out

Migrate a number you own with Orbit away to another carrier. You can submit a port-out per DID or as a batch request, then track it through its lifecycle.

Submit a port-out for a single number

A successful submission returns 201 with the provider order details (provider, provider_order_id, provider_status, foc_date).

Port-out request lifecycle

The /porting/out endpoints submit and track a batch port-out request covering up to 50 numbers.

Port-Out Protection (PIN)

Port-Out Protection guards against port hijacking (a SIM-swap variant where an attacker with captured credentials triggers a port to a carrier they control). It mirrors FCC 47 CFR § 64.6100 and GSMA M.108: when enabled on a number, every port-out submission for that number must present the matching PIN or receive PORT_OUT_PIN_MISMATCH (401) before the carrier dispatch fires. The PIN is one-way scrypt-hashed at rest with a per-number salt and is never returned by any endpoint — only the { enabled, set_at } status is observable. If you lose the PIN, rotate it with a fresh POST; there is no recovery flow.

Enable protection

The port_out_pin must be 4–32 characters. A successful set returns 201 with { enabled: true, set_at }. Disabling via DELETE returns { enabled: false }. Once protection is enabled, include the same port_out_pin in any port-out submission for the number. A missing or mismatched PIN is rejected with PORT_OUT_PIN_MISMATCH (401) before the carrier is contacted.