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:
PageWhat it covers
Inventory & ExportList numbers, bulk CSV/JSON export, low-stock alerts
Country CapabilitiesPer-country inventory summary before you search
Regulatory PreviewDocument/field requirements before you buy
Number LifecycleAuto-renew, scheduled release, reassign, reclaim, trial pool
Emergency Address (E911)Register and validate dispatchable addresses
CNAM & Caller IDBranded caller name, CNAM dip, spam-label remediation
Number Health & ReputationWarming, health, reputation, usage & branded-calling analytics
Number LookupCarrier / portability / SIM-swap / roaming intelligence

Search Available Numbers

curl "https://api.orbit.devotel.io/api/v1/numbers/available?country=US&type=local&capabilities=sms,voice" \
  -H "X-API-Key: dv_live_sk_..."

Response

The matching numbers are nested under data.numbersdata is an object, not an array. Each item’s recurring fee is monthlyRate, a numeric dollar amount (there is no separate currency field).
{
  "data": {
    "numbers": [
      {
        "id": "did_8f2a91c4",
        "phoneNumber": "+14155550100",
        "country": "US",
        "type": "local",
        "capabilities": ["sms", "voice"],
        "sms_two_way": true,
        "monthlyRate": 1.50,
        "setupRate": 0,
        "provider": "devotel"
      },
      {
        "id": "did_a13b772e",
        "phoneNumber": "+14155550101",
        "country": "US",
        "type": "local",
        "capabilities": ["sms", "voice"],
        "sms_two_way": true,
        "monthlyRate": 1.50,
        "provider": "devotel"
      }
    ],
    "providers": [
      { "provider": "devotel", "status": "ok" }
    ],
    "compliance": {},
    "pagination": {
      "page": 1,
      "limit": 100,
      "total": 2,
      "total_pages": 1,
      "has_more": false
    }
  }
}
FieldTypeDescription
idstringProvider-scoped identifier for the available number.
phoneNumberstringThe number in E.164 format.
capabilitiesstring[]Supported channels (e.g. sms, voice, mms).
sms_two_waybooleantrue when the number can both send and receive SMS.
monthlyRatenumberRecurring monthly fee in dollars.
setupRatenumber?One-time setup/activation fee in dollars. Omitted when there is no setup charge.
providerstringCarrier that supplies the number.
expectedActivationobject?When the carrier is expected to activate the number after purchase.

Purchase a Number

curl -X POST https://api.orbit.devotel.io/api/v1/numbers/purchase \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number": "+14155550100",
    "country_code": "US"
  }'

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.
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": "+14155550100" },
      { "phone_number": "+14155550101", "country_code": "US", "provider": "devotel" }
    ]
  }'

Request body

FieldTypeRequiredDescription
itemsarrayyes1–50 numbers to purchase.
items[].phone_numberstringyesNumber in E.164 format.
items[].country_codestringnoISO 3166-1 alpha-2 code. Defaults to US.
items[].providerstringnoOne of devotel, telnyx, didww. Provider hint.
items[].idstringnoProvider-issued inventory row id, when supplied by GET /numbers/available.
compliance_profile_idstringnoCompliance profile applied to every item in the batch.

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.)
{
  "data": {
    "succeeded": [
      { "id": "num_abc123", "number": "+14155550100", "status": "active" }
    ],
    "failed": [
      {
        "phone_number": "+14155550101",
        "error": { "code": "NUMBER_UNAVAILABLE", "message": "Number no longer available" }
      }
    ],
    "total_cost_cents": 300,
    "debited_cents": 150
  }
}
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.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/bulk-reserve \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "country": "US",
    "area_code": "415",
    "number_type": "local",
    "quantity": 25
  }'
FieldTypeRequiredDescription
countrystringyesISO 3166-1 alpha-2 code.
quantitynumberyesNumber of DIDs to reserve, 1–1000.
number_typestringnoOne of local, mobile, toll_free. Defaults to local.
area_codestringno2–5 digit area/region code filter.
name_patternstringnoDigits-only pattern (max 20) the number must match.
pattern_typestringnocontains (default), starts_with, or ends_with.
capabilitiesstringnoComma-separated filter of sms, mms, voice, fax.
providerstringnoPin to devotel, telnyx, or didww. Otherwise both DIDWW + Telnyx are searched.
compliance_profile_idstringnoCompliance profile reused across every item at finalize time.
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).
{
  "data": {
    "reservation_id": "resv_abc123",
    "status": "ready",
    "expires_at": "2026-06-08T12:15:00.000Z",
    "requested_quantity": 25,
    "reserved_count": 25,
    "total_cost_cents": 3750,
    "items": [
      { "phone_number": "+14155550100", "monthly_cost_cents": 150, "status": "reserved" }
    ]
  }
}

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.
curl https://api.orbit.devotel.io/api/v1/numbers/bulk-reserve/resv_abc123 \
  -H "X-API-Key: dv_live_sk_..."

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.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/bulk-reserve/resv_abc123/finalize \
  -H "X-API-Key: dv_live_sk_..."
{
  "data": {
    "id": "resv_abc123",
    "status": "finalized",
    "succeeded_count": 25,
    "failed_count": 0,
    "debited_cents": 3750
  }
}

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.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/bulk-reserve/resv_abc123/cancel \
  -H "X-API-Key: dv_live_sk_..."
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:
curl -X PUT https://api.orbit.devotel.io/api/v1/numbers/num_abc123 \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "webhook_url": "https://yourapp.com/webhooks/inbound",
    "sms_forwarding": "+14155550111"
  }'
The update body accepts only these fields: channel_config, status, label, capabilities, forwarding_number, webhook_url, sms_forwarding, compliance_profile_id, 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.

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:
curl -X PUT https://api.orbit.devotel.io/api/v1/numbers/+14155550100/routing \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "agent",
    "config": { "agentId": "agent_support_bot" },
    "sms_route_type": "agent",
    "sms_route_config": { "agentId": "agent_support_bot" }
  }'
The voice type is one of agent, softphone_user, softphone_register, ivr, voicemail, queue, sip_forward, transfer, conference, decline, or ring_group; each type has its own config shape. 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

TypeAPI type valueDescription
LocallocalGeographic number tied to a city or region
Toll-Freetoll_freeFree for callers, costs billed to you. SMS is send-only where supported (toll-free is never two-way)
MobilemobileMobile number for SMS and voice
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.
CapabilityDescriptionSearch filterPurchase field
smsSend and receive SMSyesyes
voiceMake and receive callsyesyes
mmsSend and receive MMS (images, media)yes
faxSend and receive fax (T.38)yes
whatsappRegister as WhatsApp Business numberyes
rcsRegister for RCS Business Messagingyes

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.
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/check \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "phoneNumber": "+14155551234",
    "currentCarrier": "Carrier Name"
  }'
{
  "data": {
    "eligible": true,
    "reasons": [],
    "splittable": false,
    "estimated_foc_date": "2026-06-18",
    "fast_portable": false,
    "check_status": "checked",
    "provider": "telnyx"
  }
}
FieldTypeDescription
eligiblebooleanWhether the number can be ported to Orbit.
reasonsstring[]Human-readable blockers when eligible is false; empty when eligible.
splittablebooleantrue when the carrier may port voice but leave SMS behind — include explicit SMS instructions in your LoA.
estimated_foc_datestring?Carrier’s estimated Firm Order Commitment (completion) date in ISO 8601. Present when the carrier can pre-compute it (typically the fast_portable path).
fast_portableboolean?true when the carrier flags a fast port (often same- or next-business-day).
check_statusstringchecked (live carrier verdict), check-not-supported (carrier has no synchronous check — e.g. DIDWW v3; verify eligibility manually), or check-skipped (carrier unreachable; eligible is optimistic).
providerstringCarrier selected for the check, based on the number’s country.
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:
MethodPathDescription
POST/api/v1/numbers/porting/:id/loaUpload the LoA artefact (PDF / JPEG / PNG / HEIC, multipart file field). Stores it to GCS and sets loaSignatureStatus to draft.
POST/api/v1/numbers/porting/:id/loa/signCapture the signer’s name, email, and acknowledgement; transitions loaSignatureStatus to signed.

3. Submit the port-in request

curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": ["+14155551234"],
    "currentCarrier": "Carrier Name",
    "country": "US",
    "loaFileUrl": "https://storage.googleapis.com/devotel-media/loa/...signed...",
    "accountNumber": "ACC-99182",
    "authorizedSigner": "John Doe"
  }'
FieldRequiredDescription
numbersyesOne E.164 string or an array of them.
currentCarrieryesName of the losing carrier (carrier is accepted as an alias).
countrynoISO 3166-1 alpha-2 of the numbers; drives provider selection (Telnyx US/CA, DIDWW elsewhere). Omit to store without dispatching for hand-processing.
loaFileUrlnoGCS-signed URL of the uploaded LoA (see step 2). Omitting it stores the request in manual mode.
accountNumbernoYour account / BTN with the losing carrier.
accountPinnoAccount PIN (required by some carriers, e.g. US wireless).
authorizedSignernoName of the LoA signatory.
serviceAddressnoService address (line1, line2, city, state, postalCode, countryCode) matching the losing carrier’s billing record.

4. Track the port

MethodPathDescription
GET/api/v1/numbers/portingList your port-in requests and their status.
GET/api/v1/numbers/porting/:id/timelineStructured per-stage timeline — submitted → validating_loa → carrier_review → foc_assigned → foc_scheduled → completed — with the elapsed-days counter, the FOC date once assigned, and (on rejection) a plain-English explanation of the carrier reject code plus a recommended action.
POST/api/v1/numbers/porting/:id/refreshRe-poll the losing carrier for the latest status and FOC date.
POST/api/v1/numbers/porting/:id/supplementSubmit a correction (supplement/amend) for a rejected request within the 7-day amend window — avoids restarting the FOC clock.
DELETE/api/v1/numbers/porting/:idCancel a port-in request.
curl https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/timeline \
  -H "X-API-Key: dv_live_sk_..."

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

curl -X POST https://api.orbit.devotel.io/api/v1/numbers/num_abc123/port-out \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "losing_carrier_account_number": "ACC-99182",
    "authorized_person": "John Doe",
    "target_carrier_ocn": "1234"
  }'
FieldRequiredDescription
losing_carrier_account_numberyesYour account number with the carrier currently hosting the number. Proof of ownership required before the port-out is accepted.
authorized_personyesName of the LoA signatory authorizing the release. Audit-logged and forwarded to the carrier.
target_carrier_ocnyesOperating Company Number (OCN) of the winning carrier, used to schedule the FOC.
port_out_pinconditionalRequired only when the number has Port-Out Protection enabled. A missing or wrong value returns PORT_OUT_PIN_MISMATCH (401) before any carrier dispatch.
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.
MethodPathDescription
POST/api/v1/numbers/porting/outSubmit a new port-out request
GET/api/v1/numbers/porting/outList port-out requests
GET/api/v1/numbers/porting/out/:idFetch a single port-out request
POST/api/v1/numbers/porting/out/:id/refreshRe-poll the carrier for the latest status
DELETE/api/v1/numbers/porting/out/:idCancel a port-out request
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/out \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "numbers": ["+14155551234"],
    "destinationCarrier": "Carrier Name",
    "newOwnerInfo": "Acme Inc, 123 Main St",
    "port_out_pin": "4821"
  }'
FieldRequiredDescription
numbersyes1–50 E.164 numbers from your inventory to port away. All must be hosted on the same provider.
destinationCarrieryesName of the winning carrier.
newOwnerInfonoFree-text details of the new account owner (max 2000 chars).
port_out_pinconditionalRequired when any number in the batch has Port-Out Protection enabled. A missing or wrong value returns PORT_OUT_PIN_MISMATCH (401) before carrier dispatch; numbers without protection auto-pass.

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.
MethodPathDescription
GET/api/v1/numbers/:id/port-out-protectionRead protection status ({ enabled, set_at })
POST/api/v1/numbers/:id/port-out-protectionSet or rotate the PIN
DELETE/api/v1/numbers/:id/port-out-protectionDisable protection

Enable protection

curl -X POST https://api.orbit.devotel.io/api/v1/numbers/num_abc123/port-out-protection \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "port_out_pin": "4821" }'
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.