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

# Port-out lifecycle and the ownership model

> How a number leaves Orbit: the port_out_pending state, tenant self-serve submission, PIN protection as an ownership control, the concurrency guards that prevent double ports, and the events each step emits.

# 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](/numbers/porting); the status vocabulary (`active`,
`parked`, `released`, …) is defined in [Number status
map](/concepts/number-lifecycle).

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

```
purchase / port-in   →   active   →   port_out_pending   →   (gone)
                                   ↘ cancelled / failed  →  active
```

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.*`:

| Event                                                                                               | Fires when                                                                                          |
| --------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `number.port_out.requested`                                                                         | Your port-out was accepted by the carrier and the DID moved to `port_out_pending`.                  |
| `number.port_out.cancelled`                                                                         | A pending port-out was cancelled (by you, or by an admin) and the DID returned to its prior status. |
| `porting.request.submitted` / `porting.request.loa_signed` / `porting.request.supplement_submitted` | Inbound port-in milestones (LOA signed, supplement filed).                                          |
| `porting.request.cancelled`                                                                         | A port-in request was cancelled.                                                                    |
| `porting.request.manual_review_required`                                                            | A port-in left the automated path and needs operator review.                                        |

Payload shapes and subscription setup are in [Webhook
events](/webhooks/events).

## Cross-links

* [Number status map](/concepts/number-lifecycle) — the six-state enum
  `port_out_pending` joins, and how each state filters on the list
  endpoint.
* [Number lifecycle](/numbers/lifecycle) — release, reclaim, suspend,
  and the non-port exits.
* [Number porting](/numbers/porting) — the inbound (winning-carrier)
  flow: preflight checks, LOA, and the port-in timeline.
* [Webhook events](/webhooks/events) — full payload reference for
  `number.port_out.*` and `porting.request.*`.
