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

# Number porting: LOA upload, FOC scheduling, status timeline, and bulk CSV

> Self-serve local number portability (LNP) — upload and sign a Letter of Authorization, track FOC dates on a per-stage timeline, react to porting webhooks, and migrate 50-500 DIDs at once with a bulk CSV import.

# Number Porting (LNP)

Bring numbers from another carrier onto Orbit. A port-in request moves through carrier review, a Letter of Authorization (LOA), and a Firm Order Commitment (FOC) date before the number lands on your account — this guide covers the full self-serve flow: submitting a request, attaching and signing the LOA, reading the structured status timeline, reacting to webhooks, and migrating a large DID estate in one CSV upload.

Read operations (`list`, `get`, `timeline`, the `check` / `pre-validate` preflight lints) are available to any role on the organization. Writes (`create`, LOA upload/sign/submit, `supplement`, `cancel`) require the `numbers:write` scope and an `owner`, `admin`, or `developer` role.

## Submit a 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": "Acme Telecom",
    "country": "US",
    "accountNumber": "998877",
    "authorizedSigner": "Jane Doe",
    "serviceAddress": {
      "line1": "1 Market St",
      "city": "San Francisco",
      "state": "CA",
      "postalCode": "94105",
      "countryCode": "US"
    }
  }'
```

`numbers` accepts a single E.164 string or an array for a multi-line port under one account. Omit `country` to store the request without dispatching to a carrier — useful when your team wants to attach documents before Orbit forwards it. Before submitting, run a portability preflight to catch a non-portable number early:

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

# Batch check — up to 1000 distinct numbers per call, chunked upstream to the carrier
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/check/bulk \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "phoneNumbers": ["+14155551234", "+14155555678"] }'

# Combined readiness gate (portability + CSR/address checks) before you submit
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/pre-validate \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "numbers": ["+14155551234"], "country": "US" }'
```

`pre-validate` and `check/bulk` are rate-limited to 5 requests per minute per auth context.

## Upload and Sign the LOA

The Letter of Authorization proves you're authorized to move the number. Orbit exposes a dedicated upload → sign → submit lifecycle instead of accepting a bare file URL:

```bash theme={null}
# 1. Upload the signed-by-hand LOA artefact (PDF / JPEG / PNG / HEIC, up to 10MB)
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/loa \
  -H "X-API-Key: dv_live_sk_..." \
  -F "file=@loa.pdf"

# 2. Capture an in-platform signature acknowledgement
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/loa/sign \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "signerName": "Jane Doe",
    "signerEmail": "jane@acme.com",
    "acknowledgement": "I am authorized to request this number be ported to Orbit and the information above is accurate."
  }'

# 3. Forward the signed LOA to the losing carrier
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/loa/submit \
  -H "X-API-Key: dv_live_sk_..."
```

Each step transitions `loaSignatureStatus`: upload sets it to `draft`, signing moves it to `signed`, and submit forwards it to the carrier. The state machine is one-directional — once a LOA is `signed` or `submitted`, re-uploading a replacement is refused; cancel the porting request and resubmit instead if the wrong document went out. Signing twice returns `422`, not a silent merge.

If the losing carrier kicks back a supplement request (a minor data correction — usually an address line or suite number), respond within the 7-day amend window so the FOC clock doesn't restart:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/supplement \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{ "serviceAddress": { "line2": "Suite 200", "countryCode": "US" } }'
```

## Track the Status Timeline

`GET /porting/:id/timeline` expands the flat porting status into a structured, per-stage view — `submitted → validating_loa → carrier_review → foc_assigned → foc_scheduled → completed` — so you can show operators exactly where a port is stuck without calling the losing carrier.

```bash theme={null}
curl "https://api.orbit.devotel.io/api/v1/numbers/porting/port_abc123/timeline" \
  -H "X-API-Key: dv_live_sk_..."
```

```json theme={null}
{
  "data": {
    "portingId": "port_abc123",
    "status": "reviewing",
    "stages": [
      { "stage": "submitted", "state": "completed", "label": "Submitted", "enteredAt": "2026-08-01T10:00:00.000Z" },
      { "stage": "validating_loa", "state": "completed", "label": "Validating LoA", "enteredAt": "2026-08-01T10:00:00.000Z" },
      { "stage": "carrier_review", "state": "current", "label": "Losing carrier review", "enteredAt": "2026-08-01T10:00:00.000Z" },
      { "stage": "foc_assigned", "state": "pending", "label": "FOC date assigned" },
      { "stage": "foc_scheduled", "state": "pending", "label": "Scheduled for FOC" },
      { "stage": "completed", "state": "pending", "label": "Port complete" }
    ],
    "elapsedDays": 4
  }
}
```

`focDate` is echoed on the timeline once the losing carrier assigns one. If the port is `rejected`, the response includes a `rejection` object that translates the carrier's opaque reject code (Telnyx/DIDWW/Bandwidth vocabularies, including numeric NIIF codes) into a plain-English `summary` and `recommendedAction`, tagged with a `severity` of `operator_fixable`, `needs_carrier`, or `fatal` so you know whether to fix your own data or escalate. Unknown codes fall through with the raw carrier text preserved rather than being hidden.

Poll for a fresh carrier decision with `POST /porting/:id/refresh`, or cancel outright with `DELETE /porting/:id`.

## React to Porting Webhooks

Subscribe to these events instead of polling `timeline` on a loop — see [Webhook Events](/webhooks/events) for the full catalog and [Webhook Security](/webhooks/security) for signature verification:

| Event                                    | Fires when                                                              |
| ---------------------------------------- | ----------------------------------------------------------------------- |
| `number.ported`                          | The port-in completed and the number is live on your account.           |
| `porting.request.loa_signed`             | The in-platform LOA signature was captured.                             |
| `porting.request.supplement_submitted`   | You submitted a carrier-requested supplement/amendment.                 |
| `porting.request.manual_review_required` | The port fell back to manual review (ops will process the LOA by hand). |
| `porting.request.cancelled`              | You or an admin cancelled the request.                                  |

## Bulk CSV Port-In (Enterprise Migration)

Migrating 50-500 DIDs from another carrier as a single handoff doesn't fit the one-number-at-a-time form. Upload a CSV instead:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/numbers/port-in/bulk-csv \
  -H "X-API-Key: dv_live_sk_..." \
  -F "file=@port-in-batch.csv"
```

The CSV must have a header row with exactly these columns (order doesn't matter, matching is case-insensitive):

```
phone_number, current_carrier_account_number, current_carrier_name, billing_name, billing_address
```

Each valid row becomes its own porting request (`status: submitted`, stored for manual carrier dispatch) so your team can attach the LOA and country code per-row afterwards through the normal dashboard flow — bulk import does not attempt 50-500 sequential carrier calls inside one request. The response reports both sides of the batch:

```json theme={null}
{
  "data": {
    "request_ids": ["port_a1b2c3", "port_d4e5f6"],
    "validation_errors": [
      { "row": 14, "field": "phone_number", "error": "Not a valid E.164 number" }
    ]
  }
}
```

Caps: 2MB file size (roughly 12k rows), 1000 rows per request, and a 30-second processing budget. The endpoint is rate-limited to 2 requests per minute per organization — split a larger ledger into multiple uploads rather than retrying a timed-out one.

## Set Up a Ported Number

Once the port completes, provision the number's regulated identity from the number's detail page or API before you route traffic to it:

* [Emergency Address (E911)](/numbers/emergency-address) — register and validate the per-number dispatchable emergency address when the ported number carries voice.
* [CNAM & Caller ID](/numbers/cnam) — register the branded caller name recipients see, dip the carrier's CNAM database for the currently registered name, and remediate "Spam Likely" or "Scam Likely" labels.

## See Also

* [Number Lifecycle](/numbers/lifecycle) — what happens to porting-completed numbers afterward (auto-renew, release, reassignment).
* [Numbers API Reference](/api-reference/numbers) — full request/response schemas for every porting endpoint, including toll-free RespOrg porting and hosted messaging (no-port SMS enablement).
* [Webhook Events](/webhooks/events) — the complete Number / Porting event catalog.
