Skip to main content

Port-out lifecycle and ownership model

Porting a number OUT is a transfer of ownership: your DID stops being a number Orbit bills and routes for you and becomes a number another carrier provisions for you. That is a different trust boundary than any other lifecycle operation, so port-out carries its own state, its own concurrency guards, and an optional PIN gate. This page explains how the pieces fit together. The step-by-step INBOUND flow is Number porting; the status vocabulary (active, parked, released, …) is defined in Number status map.

Where port-out fits in the number lifecycle

A DID’s life in your account runs purchase → active → (optional suspend/park) → release. Port-out is the one exit that is NOT a release: instead of handing the number back to the carrier’s inventory, you direct it to a new carrier. The chain is:
Two flows share this chain, and they differ by who drives the order:
  • Losing-carrier flow (this page). Orbit hosts the DID; the winning carrier asks for it. You initiate the move tenant-side with POST /numbers/:id/port-out, Orbit validates and dispatches a port-out order to the carrier of record, and the number sits at port_out_pending until the carrier completes the port.
  • Winning-carrier flow (port-in). Another carrier hosts the DID; you claim it into Orbit with POST /porting. The losing carrier is a third party; Orbit only tracks the request. Events on this side use the porting.request.* family.
port_out_pending is a synthetic status in the same closed enum as active, released, and the rest — it exists so the list endpoint, the dashboard badge, and every guard that checks “is this number still mine to use” can treat a mid-port DID as exactly that: claimed by an outbound order, not yet gone.

Which statuses can port out

Only a portable status can enter the flow — in practice active (and inactive). A submission against a number already in port_out_pending, released, suspended, or parked is rejected with 409 CONFLICT:
  • port_out_pending — an outbound order already exists; a second one would put two carrier orders against the same DID and produce conflicting FOC dates. Cancel the existing order first.
  • released / parked — the number is leaving or has left your account through the release path; there is nothing to port.
  • suspended — resolve the suspension (overdue balance or a manual pause) first; a soft-paused number is not portable.

PIN protection as an ownership control

Port-out is the one operation where a caller with your API credentials could permanently remove a number from your account — the classic port-hijack / SIM-swap-class attack. PIN protection adds a second factor you control, per number, mirroring the carrier “account PIN” model from FCC 47 CFR § 64.6100. The endpoints, all under your own numbers:
  • POST /numbers/:id/port-out-protection — set (or rotate) the PIN. Send the new PIN in the body; the response confirms only { enabled: true, set_at }.
  • GET /numbers/:id/port-out-protection — read back { enabled, set_at }. Nothing else is readable.
  • DELETE /numbers/:id/port-out-protection — disable protection.
  • POST /numbers/:id/port-out — when protection is enabled on the DID, the request body must carry port_out_pin. A missing or incorrect PIN is rejected with 401 PORT_OUT_PIN_MISMATCH before any carrier call is made.
Three properties make the PIN a real ownership control rather than a password-shaped hint:
  • Hashed at rest, never returned. The PIN is scrypt-hashed with a per-DID salt. Every read path — the GET above, the numbers detail payload, the port-out response — exposes only enabled and set_at. Record the PIN when you set it; if it is lost, rotate it (set a new one) rather than trying to recover it.
  • Opt-in, per DID. A DID with no PIN set verifies as a pass — the absence of protection is not a rejection. You enable it on exactly the numbers whose loss would hurt.
  • Pre-carrier gate. The verify runs before the carrier dispatch, so a wrong PIN never burns an upstream submission attempt, and the comparison is timing-safe so it cannot be probed by response-time measurement.
PINs are 4–32 characters, digits or alphanumerics with common punctuation. Every set, rotate, remove, and status read also verifies that the caller’s organization owns the DID — sibling accounts in the same workspace cannot read, change, or strip each other’s PINs.

Concurrency: one port-out order per number

Submitting a port-out talks to an external carrier, so a request can be slow and can overlap with a retry or a double-click. The flow is built so two simultaneous submissions for the SAME number can never produce two carrier orders:
  1. Atomic claim. The row flips to port_out_pending with a single conditional write that only succeeds if the status is still the portable status read moments earlier. Exactly one concurrent caller wins; the loser gets no rows back and aborts with 409 CONFLICT before anything is dispatched upstream.
  2. A short-lived distributed lock (30-second auto-expiry) serializes contenders for the same DID in the common case, and expires on its own so a crashed request can never deadlock the number. Different numbers never block each other.
  3. Carrier-side idempotency. Both carrier integrations derive their idempotency key from the DID id, so even a duplicated upstream call collapses into one carrier order.
Claiming happens before dispatch on purpose. If the carrier rejects the order or its API fails, the claim rolls back to the prior portable status — the number never gets stranded in port_out_pending with no real carrier order behind it. A 409 on your retry therefore means “already in progress”; a carrier failure means the number is back where it started and the response says no local state changed.

Carrier capability caveat

Self-serve port-out requires a port-out adapter for the carrier that hosts the DID. Adapters exist for Telnyx-hosted and DIDWW-hosted numbers. Numbers with no adapter — the default platform provider, where Orbit’s own carrier layer holds the DID directly — cannot self-serve. Submitting for one returns 422 PORT_OUT_NOT_SUPPORTED; coordinate that port-out through support, who dispatch it via the carrier’s portal and record the order id on the number for you.

Events

The outbound (losing-carrier) side emits the number.port_out.* family; the inbound (winning-carrier) side emits porting.request.*: Payload shapes and subscription setup are in Webhook events.
  • Number status map — the six-state enum port_out_pending joins, and how each state filters on the list endpoint.
  • Number lifecycle — release, reclaim, suspend, and the non-port exits.
  • Number porting — the inbound (winning-carrier) flow: preflight checks, LOA, and the port-in timeline.
  • Webhook events — full payload reference for number.port_out.* and porting.request.*.