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:- 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 atport_out_pendinguntil 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 theporting.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 practiceactive (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 carryport_out_pin. A missing or incorrect PIN is rejected with401 PORT_OUT_PIN_MISMATCHbefore any carrier call is made.
- 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
enabledandset_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.
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:- Atomic claim. The row flips to
port_out_pendingwith 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 with409 CONFLICTbefore anything is dispatched upstream. - 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.
- 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.
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 defaultplatform 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 thenumber.port_out.*
family; the inbound (winning-carrier) side emits porting.request.*:
Payload shapes and subscription setup are in Webhook
events.
Cross-links
- Number status map — the six-state enum
port_out_pendingjoins, 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.*andporting.request.*.