Port a number end-to-end
Porting is the highest-stakes move a customer makes on Orbit: get the CSR data wrong and the losing carrier rejects weeks in, and you restart the clock. This guide walks one complete port — for a single DID or a 50-1000-DID estate — from the first portability check through the LOA signature, the FOC timeline, webhook-driven ops, and post-port lifecycle. Build it once and every migration looks the same. Reads (list, get, timeline, check, pre-validate) need the numbers:read scope; writes (create, LOA upload/sign/submit, supplement, cancel) need numbers:write and an owner, admin, or developer role. The API reference is at Numbers; the shorter endpoint sequence page is at Number Porting.
1. Prep the data
Before any API call, pull a Customer Service Record (CSR) from the losing carrier — a bill or the carrier’s own LNP record that shows the exact legal entity name, account number, billing telephone number (BTN for multi-line), service address, and any transfer PIN. Rejection codes likeADDRESS_MISMATCH and NAME_MISMATCH almost always trace back to a guessed, not copied, CSR field.
Build the E.164 list of numbers to move, and note these country rules up front:
- US / CA numbers route to a live synchronous portability check (Telnyx). Everywhere else routes to DIDWW, which has no synchronous check — the verdict comes back as
check-not-supportedand you verify eligibility manually. - Toll-free numbers do not port carrier-to-carrier. They move by changing the Responsible Organization (RespOrg) via Somos; submit that at
POST /numbers/porting/toll-free/resporginstead of the LNP flow. - If the number is SMS-enabled on the losing carrier, call that out on the LOA — the response
splittable: truetells you the carrier may port voice but leave SMS behind.
2. Preflight — gate before you submit
Run preflight on every number. It is read-only, creates no order, and catches the rejections that would otherwise cost you the 2-7 business-day submission window.eligible: false— do not submit;reasonsnames the blockers (e.g. rate-center exclusion).fatalrejections must stay at the losing carrier.check_status: "check-not-supported"— DIDWW territory; verify eligibility manually before submitting.check_status: "check-skipped"— the carrier call failed and Orbit passed optimistically; never gate on this. Re-run or verify manually.splittable: true— call SMS out on the LOA explicitly.
pre-validate — runs the same portability check and statically lints the CSR fields you will send, catching the omission/mismatch class of rejections (ADM/address, name, BTN) before the order exists:
ready: true — every number portable AND no blocker-severity CSR issue. check is rate-limited to 20 req/min, check/bulk and pre-validate to 5 req/min per auth context.
3. Submit the port
Onceready, submit. numbers accepts a single E.164 string or an array for a multi-line port under one account. There are three ways a create can leave your system, and the choice matters for how the request is tracked afterwards:
- Full dispatch (default) — send
countryand nodraftflag. Orbit opens the carrier order immediately (Telnyx for US/CA, DIDWW elsewhere) and the row returns with aproviderset. This is the single-shot path the curl below takes. - Wizard checkpoint —
draft: true— store the request locally and deliberately skip carrier dispatch. The row is stampedstaging: "draft"; nothing reaches the losing carrier until the LOA is uploaded and signed on the returned row. This is the semantic the tenant-facing port-in wizard’s first step uses, and any flow you build that needs to checkpoint numbers and CSR details before a signed LOA exists. - Ops handover — omit
country— also stores the request with no carrier dispatch, but it means “ops hand-processes this port”: no provider is ever selected,refreshhas nothing to poll (it returns422), and the request stays in manual mode for its whole life. Use it when a human on your side (or ours) drives the port out-of-band.
country row is a permanent manual-mode request with no resume semantics. Picking the wrong one either strands a wizard mid-flow or sends ops a request that was never meant for hand-processing.
Wizard draft intake
The hosted port-in wizard — the Single DID form on the dashboard’s Number Porting page — checkpoints the request before an LOA exists. Building the same flow yourself is one flag:- Enter the numbers — one E.164 per line; the wizard’s single-DID form validates each line on entry. Run the preflight gate first; the dashboard offers the same eligibility check inline and blocks submission on a negative verdict.
- Check the carrier — pick the losing carrier and fill the CSR fields (account number, PIN for US wireless, authorized signer, service address). These must match the losing carrier’s records exactly or the port rejects days later.
- Save the draft —
POST /numbers/portingwithdraft: true. The row is stored locally,staging: "draft", no carrier call. Re-creating with the same number set is idempotent: instead of a second row you get the original draft back, so a wizard step-forward or a page refresh never forks the checkpoint. - Upload the LOA in-platform —
POST /numbers/porting/port_draft456/loawith the PDF/image (see LOA lifecycle). - Sign —
POST /numbers/porting/port_draft456/loa/signwith the signer name, email, and acknowledgement. - Submit —
POST /numbers/porting/port_draft456/loa/submitforwards the signed LOA. From here the timeline drives to FOC the same as a single-shot port. - A draft has no carrier-side state — the background status poller skips it and
GET /porting/:id/refreshrefuses it (422, nothing tracked at any carrier yet), so a draft can never be accidentally dispatched by polling. It only moves when you upload and sign the LOA.
Resume and submit from a draft
A draft never dispatches on its own. Turning it into a live port is the LOA lifecycle on the returned id — there is no separate “submit the draft” endpoint, and nothing to re-POST on the create route:POST /numbers/porting/:id/loa— attach the document;loaSignatureStatusbecomesdraft.POST /numbers/porting/:id/loa/sign— record the signer acknowledgement; status becomessigned. Submitting before this step, or signing twice, returns422.POST /numbers/porting/:id/loa/submit— the onlysigned-state endpoint; anything else returns422. The signed LOA attaches to the carrier order when one already exists (the normal case for port ranges that were dispatched separately).
pre-validate before step 1 if the CSR data was gathered more than a few days ago, because a draft that sat through a billing-cycle change can arrive with stale account details.
Do not park a request in draft. No FOC date is ever issued for an undispatched row: the losing carrier only starts its 7–14 business-day review once it holds an order plus a signed LOA, and a draft has neither. A wizard that stops after step 3 produces a checkpoint that ages silently while every stakeholder assumes the port is in flight — the dashboard wizard flags exactly this state as “draft — LOA pending”. If the request is genuinely blocked (signer unavailable, CSR unknown), cancel the draft and re-create it when ready rather than letting it sit.
4. LOA lifecycle — upload, sign, submit
The Letter of Authorization proves you are authorized to move the number. Orbit exposes a dedicated three-step lifecycle rather than accepting a bare file URL, and each step transitionsloaSignatureStatus:
signed or submitted, re-uploading a replacement is refused (cancel the port and resubmit if the wrong document went out). Signing twice returns 422, not a silent merge. Make the LOA a real PDF at ≥200 dpi with the signature dated within 30 days, every number in the port range listed, and the authorized-signer name + title legible — LOA_INVALID rejections cluster here. The acknowledgment text and signer details are your organization’s own authorization record; Orbit conveys it to the carrier and stores it for your audit trail — it does not certify authorization on your behalf.
5. Supplier response loop — supplements and refresh
If the losing carrier kicks back a supplement request (a minor data correction — an address line, a suite number), respond within the 7-day amend window so the FOC clock does not restart:422 when the request is not in a rejected or supplement-submitted state, and 409 when the carrier refuses (amend window expired or order already completed).
Poll the provider for a fresh carrier decision with:
country) return 422 here — there is no carrier order to refresh. Cancel outright with DELETE /porting/:id; that is only possible while in submitted or reviewing — it returns 409 once the losing carrier has approved.
6. Track — timeline stages and FOC
GET /porting/:id/timeline expands the flat status enum into a structured per-stage view — a pure read on the stored row, no carrier call:
submitted → submitted, reviewing → carrier_review (LOA already validated), approved → foc_assigned (or foc_scheduled when focDate is set), completed → completed, rejected → snapshot at whichever stage it died at. focDate is echoed once the losing carrier assigns one. On a rejected port, stages after the current one come back skipped so a status dashboard does not show a ghost-future.
When status is rejected, the response carries a rejection object that translates the carrier’s opaque reject code (Telnyx ADM/NMM, Bandwidth 5005/5006, DIDWW ADDRESS_DOES_NOT_MATCH, numeric NIIF codes, and more) into a plain-English summary and recommendedAction, tagged with a severity:
operator_fixable— fix your own data and resubmit inside the amend window (address/name/PIN/BTN/supplement issues).needs_carrier— contact the losing carrier to resolve (unrecognized account number, conflicting order).fatal— the number cannot be ported at all (NUMBER_NOT_PORTABLE); keep it at the losing carrier.
7. Webhook-driven ops — build a status dashboard
Subscribe to these events instead of pollingtimeline on a loop — register once, and Orbit pushes each transition to your endpoint (see Webhooks overview and Webhook Security for signature verification):
Compose these into a status dashboard by joining the event stream with the timeline endpoint — events drive the realtime updates, and a daily
GET /porting + timeline sweep reconciles anything missed (deliveries are at-least-once, so dedupe on the event id). That combination gives you a live board of “where is every port stuck and what do I do about it” without a carrier call per page load.
Dashboard vs. API path: the dashboard renders the same timeline and rejection explanation in the porting wizard, and bulk CSV upload has a UI twin — use it for a one-off operator-led migration. Use the API when you need the gate (pre-validate) in a pipeline, the event stream wired into your own ops tooling, or a 500-DID estate you cannot click through. Both paths read the same underlying request, so you can mix them — submit by API, watch in the dashboard.
8. Bulk CSV migration — 50-1000 DIDs at once
Migrating a whole DID estate as one handoff does not fit the one-number-at-a-time form. Upload a CSV instead:- Quote any field containing a comma (
billing_addressalmost always needs it). A header missing a required column aborts the whole upload with422and names the missing columns. - Each valid row becomes its own porting request (
status: submitted, stored for manual carrier dispatch) — bulk import does not fire 50-1000 sequential carrier calls inside one request. Your team attaches the LOA and country code per-row afterwards through the normal dashboard flow. - 2MB file size (roughly 12k rows), 1000 rows per request, 30-second processing budget, rate-limited to 2 requests per minute per organization. Split a larger estate across uploads rather than retrying a timed-out one. Duplicate submissions against an active in-flight request are skipped; the response reports both sides of the batch:
9. After the port — lifecycle, hosted messaging, RespOrg
Oncenumber.ported fires:
- The number enters the normal lifecycle — auto-renew, scheduled release, reassignment, and reclaim all apply to ported numbers the same as purchased ones. See Number Lifecycle.
- SMS on a ported DID behaves exactly like a purchased DID on Orbit — no extra step.
- Hosted messaging / text-enable without porting — if you deliberately kept voice with the losing carrier (or the number was not voice-portable), you can still text-enable it via
POST /numbers/hosted-messaging. That is a carrier-side SMS route with its own LOA, not a port. - Toll-free post-port administration lives under the RespOrg flow (
/porting/toll-free/resporg— you re-home the number, not port it). Route changes go through Somos out-of-band.
pre-validate to ready: true → submit (or checkpoint with draft: true when the LOA is still outstanding) → upload/sign/submit LOA → subscribe the five events → track on the timeline → react to number.ported → hand off to lifecycle. That is the whole loop.
See Also
- Number Porting endpoint sequence — the shorter endpoint-by-endpoint page.
- Number Lifecycle — auto-renew, release, reassignment for completed ports.
- Numbers API Reference — full request/response schemas including RespOrg and hosted messaging.
- Webhook Events — complete event catalog.