Skip to main content

Number portability model

Local number portability (LNP) is the one lifecycle operation that moves a DID between carriers without the digits leaving service. Orbit models both directions as the same object — a porting request against a set of numbers — driven from opposite sides of the carrier boundary. This page defines that one model: who owns the number at each stage, the shared vocabulary, and how a port relates to the number status map. The endpoint-by-endpoint reference stays in Number porting; this page owns the model only.

The two carrier directions

Every port has a winning carrier (the one gaining the number) and a losing carrier (the one giving it up). Which direction you are driving determines what Orbit can do for you:
  • Winning-carrier flow (port-in). Another carrier hosts the DID today; you claim it into Orbit with POST /numbers/porting. Orbit is the winning carrier: it files the LOA with the losing carrier, tracks the FOC date, and provisions the number onto your account when the port completes. The losing carrier is a third party; Orbit polls and reacts to its decisions.
  • Losing-carrier flow (port-out). Orbit hosts the DID today; the winning carrier (your next provider) comes asking for it. You initiate the move tenant-side with POST /numbers/:id/port-out; Orbit validates, dispatches the order upstream, and holds the number at port_out_pending until the winning carrier completes the port.
These are one model, not two unrelated features. Both directions operate on the same nouns — a porting request, an LOA, an FOC date, a completion — and both converge on the same ownership handoff. The difference is which side of the handoff you sit on, and therefore which of the two sides Orbit can automate for you. The port-out internals (PIN gate, concurrency guards, carrier adapters) are covered in Port-out lifecycle and ownership model; the step-by-step port-in flow is Number porting.

The unified state vocabulary

A port in either direction moves through the same milestones:
  • submitted — the request exists and has been validated. Port-in stores the request even before dispatch if you omit country; port-out dispatches immediately after its pre-carrier guards pass.
  • carrier review — the losing carrier holds the request and verifies the account data (account number, authorized signer, service address). A mismatch produces a supplement request or a rejection, not a silent stall.
  • LOA lifecycle — the Letter of Authorization proving you may move the number: draft after upload, signed after the in-platform signature, submitted once forwarded to the carrier. One-directional — a signed or submitted LOA refuses replacement; cancel and resubmit instead.
  • FOC scheduled — the losing carrier has assigned a Firm Order Commitment date. From here the port completes on that date unless one side cancels or the carrier rejects.
  • completed — the winning carrier provisions the number. Port-in ends with the DID landing on your Orbit account as active; port-out ends with the DID leaving Orbit’s inventory entirely.
The cancelling side is symmetric with the six-state lifecycle: on the port-out side, port_out_pending → cancelled/failed → active — the number returns to exactly the state it left (Port-out lifecycle and ownership model). On the port-in side, a cancelled request simply never provisions anything.

The ownership boundary: a port both is and is not a release

A completed port-out removes the number from your account, but it is NOT a release, and the distinction matters:
  • Release hands the digits back to the carrier’s inventory. The number enters the aging window in Number reuse and aging before anyone can re-buy it — carrier inventory logic.
  • Port-out directs the digits to a specific new carrier of your choosing. The number never enters reuse or aging; it stops existing in Orbit’s inventory logic the moment the port completes.
While a port-out is pending, the DID is neither releasable nor usable inventory: port_out_pending is a synthetic status in the same closed enum as active, released, and the rest of the number status map. The six-state page lists the states a number settles into; port_out_pending is the seventh member that only exists mid-transition, so the list endpoint, the dashboard badge, and every “is this number still mine to use” guard can treat a claimed DID uniformly. It has two exit edges, both reversible in the ordinary case: completion (the number is gone) or cancel/failure (the number returns to active). Port-in mirrors this from the outside: until the port completes, no Orbit number row exists at all.

Preflight: ask before you file

A port filed against a non-portable number wastes carrier review time and restarts the FOC clock if the losing carrier kicks it back. Three read endpoints let you gate a submission before you pay that cost (full request/response shapes in Number porting):
  • POST /numbers/porting/check — single-number portability verdict.
  • POST /numbers/porting/check/bulk — up to 1000 distinct numbers per call, chunked upstream to the carrier so one bad number never blocks the batch.
  • POST /numbers/porting/pre-validate — the combined readiness gate: portability plus CSR/address checks, so the data mismatch classes that cause supplement loops surface before the LOA goes out.
pre-validate and check/bulk are rate-limited to 5 requests per minute per auth context. Run preflight on the estate first, then file one port-in request per account with the winning carrier.

Events: one family, one fan-out

Ports emit webhook events so you subscribe instead of polling a timeline: Port milestones also fan out to the in-product Notification Center under the messaging bell category — the same category sender-ID review and port status changes use — so ops users see a port move without wiring a webhook at all (Notification model). Payload shapes and subscription setup live in Webhook events.

Port-side ownership guards

The losing-carrier direction carries the heavier controls because a caller with your API credentials could otherwise remove a number permanently:
  • PIN protection — an optional per-DID PIN checked before any carrier dispatch, mirroring the carrier account-PIN model. Set, read back (enabled/set_at only), rotate, or disable under POST|GET|DELETE /numbers/:id/port-out-protection; submissions against a protected DID without the PIN are rejected before upstream.
  • Concurrency guards — an atomic status claim, a short-lived distributed lock, and carrier-derived idempotency keys guarantee one carrier order per number no matter how requests overlap. A 409 on retry means “already in progress,” never two orders.
Both are detailed in Port-out lifecycle and ownership model.

Bulk CSV migration

For an estate of 50–500 DIDs, the one-number-at-a-time form doesn’t fit: upload a CSV (POST /numbers/port-in/bulk-csv) where each valid row becomes its own porting request at submitted status, stored for manual carrier dispatch. Attach LOA and country data per-row afterwards through the normal flow — the import deliberately avoids firing hundreds of sequential carrier calls inside one request. Caps: 2MB file, 1000 rows, 30-second budget; rate-limited to 2 requests per minute per organization.

Worked example: a US port-in, and its mirror on the losing side

A US port from preflight to completion, wire by wire:
  1. Preflight. POST /numbers/porting/pre-validate with ["+14155551234"], country: "US" confirms the number is portable and the CSR/address data will survive carrier review.
  2. Submit. POST /numbers/porting files the request with the account number, authorized signer, and service address. The request enters submitted.
  3. LOA. Upload the artefact, capture the in-platform signature (loaSignatureStatus: draft → signed), then submit it to the losing carrier. Each step emits porting.request.* events.
  4. Carrier review → FOC. The losing carrier verifies the data and assigns a FOC date. GET /numbers/porting/:id/timeline turns that into a per-stage view (submitted → validating_loa → carrier_review → foc_assigned → foc_scheduled → completed), and porting.request.status_changed fires on each carrier move.
  5. Completed. On the FOC date the number lands on your account as active; number.ported fires.
The matching trace on the losing side, if you were porting the same number OUT instead: POST /numbers/:id/port-out (with port_out_pin if protection is enabled) claims the row atomically to port_out_pending, dispatches the order, and fires number.port_out.requested. The number sits claimed-but-yours until the winning carrier completes the port; a cancel returns it to active and fires number.port_out.cancelled. Either way, the number never touches the release/aging machinery.