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

# Phone Numbers

> Search, purchase, and manage virtual phone numbers through Orbit

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

| Page                                                   | What it covers                                                 |
| ------------------------------------------------------ | -------------------------------------------------------------- |
| [Inventory & Export](/numbers/inventory-export)        | List numbers, bulk CSV/JSON export, low-stock alerts           |
| [Country Capabilities](/numbers/country-capabilities)  | Per-country inventory summary before you search                |
| [Regulatory Preview](/numbers/regulatory-preview)      | Document/field requirements before you buy                     |
| [Number Lifecycle](/numbers/lifecycle)                 | Auto-renew, scheduled release, reassign, reclaim, trial pool   |
| [Emergency Address (E911)](/numbers/emergency-address) | Register and validate dispatchable addresses                   |
| [CNAM & Caller ID](/numbers/cnam)                      | Branded caller name, CNAM dip, spam-label remediation          |
| [Number Health & Reputation](/numbers/health)          | Warming, health, reputation, usage & branded-calling analytics |
| [Number Lookup](/numbers/lookup)                       | Carrier / portability / SIM-swap / roaming intelligence        |

## Search Available Numbers

```bash theme={null}
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.numbers` — `data` 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.

```json theme={null}
{
  "data": {
    "numbers": [
      {
        "id": "did_8f2a91c4",
        "phone_number": "+14155550100",
        "country": "US",
        "type": "local",
        "capabilities": ["sms", "voice"],
        "monthly_cost": "1.50",
        "setup_cost": "0.00",
        "currency": "USD",
        "provider": "devotel",
        "from_platform_inventory": false,
        "expected_activation": { "kind": "instant" },
        "requires_registration": false
      },
      {
        "id": "did_a13b772e",
        "phone_number": "+493055550101",
        "country": "DE",
        "type": "local",
        "capabilities": ["sms", "voice"],
        "monthly_cost": "3.20",
        "setup_cost": "12.00",
        "currency": "USD",
        "provider": "devotel",
        "from_platform_inventory": false,
        "expected_activation": { "kind": "review", "typicalMinutes": 1440, "maxHours": 72 },
        "requires_registration": true
      }
    ],
    "providers": [
      { "provider": "devotel", "status": "ok" }
    ],
    "compliance": {},
    "pagination": {
      "page": 1,
      "limit": 100,
      "total": 2,
      "total_pages": 1,
      "has_more": false
    }
  }
}
```

| Field                     | Type       | Description                                                                                                                                                                                                 |
| ------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                      | string     | Provider-scoped identifier for the available number. Use this (not `phone_number`) as the selection / bulk-purchase key — placeholder rows for one country can share a `phone_number` prefix.               |
| `phone_number`            | string     | The number in E.164 format.                                                                                                                                                                                 |
| `country`                 | string     | ISO 3166-1 alpha-2 country code.                                                                                                                                                                            |
| `type`                    | string     | `local`, `toll_free`, or `mobile`.                                                                                                                                                                          |
| `capabilities`            | string\[]  | Supported channels (e.g. `sms`, `voice`, `mms`, `fax`).                                                                                                                                                     |
| `monthly_cost`            | string     | Recurring monthly fee as a decimal string in `currency` (e.g. `"1.50"`).                                                                                                                                    |
| `setup_cost`              | string     | One-time setup / activation fee as a decimal string. Always present — `"0.00"` when there is no setup charge.                                                                                               |
| `currency`                | string     | ISO 4217 currency of `monthly_cost` and `setup_cost` (currently `"USD"`).                                                                                                                                   |
| `provider`                | string     | Platform provider label (`devotel`). The upstream wholesale carrier is not exposed.                                                                                                                         |
| `from_platform_inventory` | boolean    | `true` when the number comes from Orbit's own pre-provisioned pool rather than a live carrier search.                                                                                                       |
| `expected_activation`     | object     | When the carrier is expected to activate the number after purchase. `{ "kind": "instant" }` activates in seconds; `{ "kind": "review", "typicalMinutes": …, "maxHours": … }` is held for regulatory review. |
| `requires_registration`   | boolean    | `true` when the number's country requires identity / address documents at purchase (e.g. `DE`, `GB`, `FR`). Complete a [regulatory profile](/numbers/regulatory-preview) first.                             |
| `distance_miles`          | number?    | Present only for geo-proximity searches (`near_lat_lng` / `near_postal_code`): rounded distance from the search target.                                                                                     |
| `feature_status`          | object\[]? | Present when a capability is carrier-published but not yet fully provisioned (the dashboard renders a "Coming soon" badge).                                                                                 |
| `is_placeholder`          | boolean?   | `true` when the row represents a DID group / prefix rather than a single concrete number.                                                                                                                   |
| `is_beta`                 | boolean?   | `true` when the carrier flagged the row as beta inventory.                                                                                                                                                  |

Fields prefixed with an underscore (e.g. `_provider`) are internal and may change without notice — do not depend on them.

## Purchase a Number

```bash theme={null}
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.

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

### Request body

| Field                   | Type   | Required | Description                                                                  |
| ----------------------- | ------ | -------- | ---------------------------------------------------------------------------- |
| `items`                 | array  | yes      | 1–50 numbers to purchase.                                                    |
| `items[].phone_number`  | string | yes      | Number in E.164 format.                                                      |
| `items[].country_code`  | string | no       | ISO 3166-1 alpha-2 code. Defaults to `US`.                                   |
| `items[].provider`      | string | no       | One of `devotel`, `telnyx`, `didww`. Provider hint.                          |
| `items[].id`            | string | no       | Provider-issued inventory row id, when supplied by `GET /numbers/available`. |
| `compliance_profile_id` | string | no       | Compliance 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.)

```json theme={null}
{
  "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.

```bash theme={null}
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
  }'
```

| Field                   | Type   | Required | Description                                                                         |
| ----------------------- | ------ | -------- | ----------------------------------------------------------------------------------- |
| `country`               | string | yes      | ISO 3166-1 alpha-2 code.                                                            |
| `quantity`              | number | yes      | Number of DIDs to reserve, 1–1000.                                                  |
| `number_type`           | string | no       | One of `local`, `mobile`, `toll_free`. Defaults to `local`.                         |
| `area_code`             | string | no       | 2–5 digit area/region code filter.                                                  |
| `name_pattern`          | string | no       | Digits-only pattern (max 20) the number must match.                                 |
| `pattern_type`          | string | no       | `contains` (default), `starts_with`, or `ends_with`.                                |
| `capabilities`          | string | no       | Comma-separated filter of `sms`, `mms`, `voice`, `fax`.                             |
| `provider`              | string | no       | Pin to `devotel`, `telnyx`, or `didww`. Otherwise both DIDWW + Telnyx are searched. |
| `compliance_profile_id` | string | no       | Compliance 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).

```json theme={null}
{
  "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.

```bash theme={null}
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.

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/bulk-reserve/resv_abc123/finalize \
  -H "X-API-Key: dv_live_sk_..."
```

```json theme={null}
{
  "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.

```bash theme={null}
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`:

```bash theme={null}
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`, `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:

```bash theme={null}
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": "agt_support_bot" },
    "sms_route_type": "agent",
    "sms_route_config": { "agentId": "agt_support_bot" }
  }'
```

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 `5`–`120` (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 `5`–`900` (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`
  (`1`–`10`, 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

| Type          | API `type` value | Description                                                                                              |
| ------------- | ---------------- | -------------------------------------------------------------------------------------------------------- |
| **Local**     | `local`          | Geographic number tied to a city or region                                                               |
| **Toll-Free** | `toll_free`      | Free for callers, costs billed to you. SMS is **send-only** where supported (toll-free is never two-way) |
| **Mobile**    | `mobile`         | Mobile 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.

| Capability | Description                          | Search filter | Purchase field |
| ---------- | ------------------------------------ | :-----------: | :------------: |
| `sms`      | Send and receive SMS                 |      yes      |       yes      |
| `voice`    | Make and receive calls               |      yes      |       yes      |
| `mms`      | Send and receive MMS (images, media) |      yes      |        —       |
| `fax`      | Send and receive fax (T.38)          |      yes      |        —       |
| `whatsapp` | Register as WhatsApp Business number |       —       |       yes      |
| `rcs`      | Register for RCS Business Messaging  |       —       |       yes      |

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

```bash theme={null}
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"
  }'
```

```json theme={null}
{
  "data": {
    "eligible": true,
    "reasons": [],
    "splittable": false,
    "estimated_foc_date": "2026-06-18",
    "fast_portable": false,
    "check_status": "checked",
    "provider": "telnyx"
  }
}
```

| Field                | Type      | Description                                                                                                                                                                                                  |
| -------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `eligible`           | boolean   | Whether the number can be ported to Orbit.                                                                                                                                                                   |
| `reasons`            | string\[] | Human-readable blockers when `eligible` is `false`; empty when eligible.                                                                                                                                     |
| `splittable`         | boolean   | `true` when the carrier may port voice but leave SMS behind — include explicit SMS instructions in your LoA.                                                                                                 |
| `estimated_foc_date` | string?   | 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_portable`      | boolean?  | `true` when the carrier flags a fast port (often same- or next-business-day).                                                                                                                                |
| `check_status`       | string    | `checked` (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). |
| `provider`           | string    | Carrier 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](#4-track-the-port) 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:

| Method | Path                                     | Description                                                                                                                                                                  |
| ------ | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST` | `/api/v1/numbers/porting/:id/loa`        | Upload 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/sign`   | Capture the signer's name, email, and acknowledgement; transitions `loaSignatureStatus` to `signed`.                                                                         |
| `POST` | `/api/v1/numbers/porting/:id/loa/submit` | Forward the signed LoA to the losing carrier and transition `loaSignatureStatus` to `submitted`. Required to complete the flow — signing alone does not release the numbers. |

### 3. Submit the port-in request

```bash theme={null}
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"
  }'
```

| Field              | Required | Description                                                                                                                                          |
| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `numbers`          | yes      | One E.164 string or an array of them.                                                                                                                |
| `currentCarrier`   | yes      | Name of the losing carrier (`carrier` is accepted as an alias).                                                                                      |
| `country`          | no       | ISO 3166-1 alpha-2 of the numbers; drives provider selection (Telnyx US/CA, DIDWW elsewhere). Omit to store without dispatching for hand-processing. |
| `loaFileUrl`       | no       | GCS-signed URL of the uploaded LoA (see step 2). Omitting it stores the request in manual mode.                                                      |
| `accountNumber`    | no       | Your account / BTN with the losing carrier.                                                                                                          |
| `accountPin`       | no       | Account PIN (required by some carriers, e.g. US wireless).                                                                                           |
| `authorizedSigner` | no       | Name of the LoA signatory.                                                                                                                           |
| `contactName`      | no       | Name of the contact for this port-in request. Many carriers expect a contact on the LoA, so supplying it helps avoid rejections.                     |
| `contactEmail`     | no       | Email address for port-in correspondence. Must be a valid email when provided.                                                                       |
| `serviceAddress`   | no       | Service address (`line1`, `line2`, `city`, `state`, `postalCode`, `countryCode`) matching the losing carrier's billing record.                       |

### 4. Track the port

| Method   | Path                                     | Description                                                                                                                                                                                                                                                                                |
| -------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET`    | `/api/v1/numbers/porting`                | List your port-in requests and their status.                                                                                                                                                                                                                                               |
| `GET`    | `/api/v1/numbers/porting/:id/timeline`   | Structured 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/refresh`    | Re-poll the losing carrier for the latest status and FOC date.                                                                                                                                                                                                                             |
| `POST`   | `/api/v1/numbers/porting/:id/supplement` | Submit a correction (supplement/amend) for a rejected request within the 7-day amend window — avoids restarting the FOC clock.                                                                                                                                                             |
| `DELETE` | `/api/v1/numbers/porting/:id`            | Cancel a port-in request.                                                                                                                                                                                                                                                                  |

```bash theme={null}
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

```bash theme={null}
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"
  }'
```

| Field                           | Required    | Description                                                                                                                                                                                |
| ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `losing_carrier_account_number` | yes         | Your account number with the carrier currently hosting the number. Proof of ownership required before the port-out is accepted.                                                            |
| `authorized_person`             | yes         | Name of the LoA signatory authorizing the release. Audit-logged and forwarded to the carrier.                                                                                              |
| `target_carrier_ocn`            | yes         | Operating Company Number (OCN) of the winning carrier, used to schedule the FOC.                                                                                                           |
| `port_out_pin`                  | conditional | Required **only** when the number has [Port-Out Protection](#port-out-protection-pin) 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.

| Method   | Path                                      | Description                               |
| -------- | ----------------------------------------- | ----------------------------------------- |
| `POST`   | `/api/v1/numbers/porting/out`             | Submit a new port-out request             |
| `GET`    | `/api/v1/numbers/porting/out`             | List port-out requests                    |
| `GET`    | `/api/v1/numbers/porting/out/:id`         | Fetch a single port-out request           |
| `POST`   | `/api/v1/numbers/porting/out/:id/refresh` | Re-poll the carrier for the latest status |
| `DELETE` | `/api/v1/numbers/porting/out/:id`         | Cancel a port-out request                 |

```bash theme={null}
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"
  }'
```

| Field                | Required    | Description                                                                                                                                                                                              |
| -------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `numbers`            | yes         | 1–50 E.164 numbers from your inventory to port away. All must be hosted on the same provider.                                                                                                            |
| `destinationCarrier` | yes         | Name of the winning carrier.                                                                                                                                                                             |
| `newOwnerInfo`       | no          | Free-text details of the new account owner (max 2000 chars).                                                                                                                                             |
| `port_out_pin`       | conditional | Required 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.

| Method   | Path                                      | Description                                    |
| -------- | ----------------------------------------- | ---------------------------------------------- |
| `GET`    | `/api/v1/numbers/:id/port-out-protection` | Read protection status (`{ enabled, set_at }`) |
| `POST`   | `/api/v1/numbers/:id/port-out-protection` | Set or rotate the PIN                          |
| `DELETE` | `/api/v1/numbers/:id/port-out-protection` | Disable protection                             |

### Enable protection

```bash theme={null}
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](#port-out) for the number. A missing or mismatched PIN is
rejected with `PORT_OUT_PIN_MISMATCH` (401) before the carrier is contacted.
