Skip to main content

Batch port-in: one shipment, many numbers

A single-DID port teaches the state machine; a real migration — a retailer moving twelve regional storefront lines, an org folding an acquired carrier account into Orbit — fails on the coordination, not the mechanics. This guide orients a multi-number transfer around the porting dashboard and API as one staged shipment: it picks the right path per numbers, runs the bulk checklist in the right order, tracks the whole batch without polling, and closes out post-cutover provisioning deliberately. Every stage ships as a runnable request group in cURL, Node.js, Python, Go, Ruby, and PHP, all driven by one worked 12-number dataset. Reads (check, check/bulk, pre-validate, list, timeline) need the numbers:read scope; writes (create, LOA upload/sign/submit, bulk CSV import, cancel) need numbers:write and an owner, admin, or developer role. The per-endpoint runbook is Port a number end-to-end; this page is the batch-oriented layer on top of it.

1. Two lifecycles: per-number goals vs. one shipment

Every number you move still travels the one-directional port state machine — submitted → reviewing → approved → completed, with rejected and cancelled as terminal branches — and the LOA rides on top (draft → signed → submitted). A batch changes nothing about that machine; what changes is how many rows hold it and who dispatches them:
  • A one-time transfer moves one number through the wizard or POST /numbers/porting with a full dispatch — the carrier order opens when the request lands, and the LOA attaches after.
  • A batch shipment holds many numbers as one migration: preflight the whole list, import the CSV, stage the carrier handoff, and drive the per-row LOA lifecycle deliberately. Each row in the batch is its own porting request (status: submitted) with its own state — the shipment is your bookkeeping, not a carrier concept.
Two batch-specific rules fall out of that:
  • Bulk import never auto-dispatches. POST /numbers/port-in/bulk-csv stores one request per valid row and stops there — no carrier call fires inside the upload. The carrier handoff happens when your team attaches the LOA and country per row through the normal flow. That gap between “rows stored” and “rows dispatched” is the staging window this whole guide is organized around.
  • International rules re-verify per number, not per shipment. US/CA numbers get a live synchronous portability check; numbers in DIDWW territory come back check-not-supported no matter how many times you ask, and eligibility is verified manually per number. A mixed-country batch splits into two verification regimes — plan the shipment so every DIDWW row’s manual confirmation is done before any row dispatches, not discovered after the LOA is signed.
Shipment-scoped behavior in one sentence: the batch is a staging and tracking construct; authorization and eligibility stay per-number.

2. When each path applies

Pick the path before you prep the CSV: The 50-number line is where the single-request shape stops paying: the bulk CSV import caps at 1000 rows per request with a 2MB file budget, and above a single port’s one-carrier-account assumption breaks down anyway — a retail chain’s twelve regional lines often sit on three losing-carrier accounts, which forces one request per account regardless of size.

3. Bulk checklist — the shipment order, runnable

Run these stages in order. Each stage protects the next: preflight gates the CSV, the CSV gates staging, staging gates the LOA wave. The runnable dataset for every snippet in this section is a retailer moving 12 storefront lines off three losing-carrier accounts: Eleven US lines take the live synchronous portability check; the one Canadian line comes back check-not-supported and goes on the manual-verification plan from the start. The billing entity on every row is Cascade Retail Group LLC, copied verbatim from each CSR.

Stage 1 — Preflight the whole list

Before importing anything, run the pasted list through POST /numbers/porting/check/bulk. The dashboard’s Pre-flight portability review card on the Number Porting page calls this same endpoint; the API form returns a per-number verdict plus a summary rollup (total / eligible / ineligible / not_supported / skipped) for pipeline use. Drop ineligible rows from the shipment now; a non-portable DID discovered here costs nothing, discovered after the LOA it costs the carrier’s review window. The sample checks one representative number per account, including the DIDWW-territory line:
The two US lines come back carrier-confirmed; the Toronto line is eligible for the shipment but on the manual regime:
200
Plan the shipment so the manual row’s carrier confirmation lands before its wave dispatches (Stage 4), not after.

Stage 2 — Lint the CSR fields with pre-validate

POST /numbers/porting/pre-validate is the combined gate: it runs the portability check and statically lints the CSR fields you intend to submit, one call per losing-carrier account. Gate on ready: true before the CSV ships; in a batch this is where an account-number transposition across thirty rows surfaces before it submits, not after.
A passing account returns ready: true with an empty issue list:
200
Repeat the call for accounts 445566 and 90213. When a field disagrees with the carrier’s record, the gate closes and the issue names the field to fix; correct the CSR copy and re-run before the row enters the CSV:
422 NAME_MISMATCH

Stage 3 — Import the CSV

Build the file with exactly these five columns (order irrelevant, header matching case-insensitive), one row per number, and quote any field containing a comma (billing_address almost always needs it):
Upload it as multipart/form-data under the file field. Caps: 2 MB per file, 1000 rows per request, a 30-second processing budget, and 2 requests per minute per organization; a header missing a required column aborts the whole upload with 422 and names the missing columns. Split a larger estate across uploads rather than retrying a timed-out one.
Every valid row becomes its own stored porting request, status: submitted, nothing dispatched. The response reports both sides of the batch: the per-row requests (keep the ids; the LOA wave in Stage 4 runs on them), rows skipped because an identical number is already in flight, and the rows to fix and re-upload. A corrected re-upload is safe; duplicates are skipped, not re-created:
200
Here row 13 (the Toronto line) had a typo on first upload and one Frontier row was already in flight from an earlier attempt — fix the row, re-upload just that line, and read validation_errors per row until it is empty.

Stage 4 — Drive the LOA wave per row

The rows are stored and staged; dispatch is deliberate. File the LOA per row on the schedule you control: wave 1 ships the four Frontier numbers, wave 2 (day +2) the Consolidated block, wave 3 the Toronto line after its manual confirmation lands. A CSR mismatch on wave 1 then corrects the remaining waves’ CSV before they dispatch, instead of rejecting twelve rows in parallel. Per row the lifecycle is three sequential calls on the request id from Stage 3: upload → sign → submit. One LOA must list exactly the number on that row, signed within 30 days, legible at ≥200 dpi. The sample walks the wave-1 first row, port_rq01:
Each call returns the row with the LOA one state further along:
Upload — 201
Sign — 200
Submit — 200
The state machine only moves forward: once the LOA is signed or submitted, a replacement upload is refused — cancel the port and resubmit with the corrected document. Do not park a staged row: a stored request that never gets its LOA never reaches the carrier, never gets a FOC, and ages silently. Repeat the triple for every row in the wave before opening the next one.

Stage 5 — Reconcile with list and timeline

GET /numbers/porting is the shipment board; GET /numbers/porting/{id}/timeline answers “where is row N stuck” per row without a carrier call. Run the pair as a nightly sweep; anything a webhook delivery missed surfaces here.
The timeline expands the flat status into per-stage entries with timestamps; focDate appears the moment the carrier assigns one:
200 — timeline for port_rq05
POST /numbers/porting/{id}/refresh re-polls the losing carrier for one row; treat it as a targeted operator action on a row that looks stale, not a scheduling primitive. A cron that fans refresh out over a whole batch is a carrier call per page load; the events-plus-sweep pattern in Stage 6 is the one that scales.

Stage 6 — Webhook board, dedupe on event id

Register one endpoint for the five porting events and let each delivery move a row on your ops board instead of polling:
Delivery is at-least-once, so dedupe on the event id in your receiver and treat number.ported as the terminal success signal. Events feed your own tooling; the dashboard is the operator’s live board; the Stage 5 list+timeline pair is the nightly reconciliation. All three read the same underlying requests — submit by API, watch in the dashboard.

4. Post-cutover checklist — per row, then per shipment

number.ported fires exactly once per completed port. Treat it as the start of the cutover work, not the end: Per row (the number itself):
  • Re-verify provisioning on the number — SMS on a ported DID behaves identically to a purchased one, but only provision what the shipment intends: if voice stayed with the losing carrier deliberately, the SMS side is a hosted-messaging LOA (POST /numbers/hosted-messaging), not a port. Confirm which regime each row is in before declaring the row done.
  • Re-verify what the carrier actually split — a preflight splittable: true verdict means the carrier may port voice and leave SMS behind; check each number.ported row against that expectation, and text-enable via hosted messaging when the split left SMS behind intentionally.
  • Assign the number into your routing — a completed port does not attach the number to a voice flow, agent, or campaign; that step is yours.
Per shipment (the migration):
  • Sweep the batch against the preflight list — every number you shipped should be either completed or explicitly accounted for (rejected with a supplement in flight, or cancelled on purpose). A shipment is not closed while a row sits in an unexplained state.
  • International rows get a second pass — DIDWW-territory numbers verified manually before dispatch deserve a manual confirmation post-cutover too: dial the number, send an SMS to it, and check inbound routing before removing the old carrier from the account.
  • Toll-free is not on this list — toll-free numbers never travel the LNP flow; a RespOrg change via POST /numbers/porting/toll-free/resporg is a different lifecycle with its own Somos-side administration. Keep toll-free out of the CSV shipment and run it as its own parallel migration.
  • Close the losing-carrier account deliberately — the port completing does not close anything on the losing side; that confirmation is your migration ledger’s last line.

Worked example: the twelve regional lines, end to end

The dataset that runs through the stages above is the whole shipment: 12 E.164 lines, 11 US, 1 CAN, three losing-carrier accounts, one row per number in the CSV.
  1. Build the list — the 12 lines above; the Toronto line goes on the manual-verification plan from the start (DIDWW territory, check-not-supported expected).
  2. Pull three CSRs — one per losing-carrier account; each storefront’s billing address copied verbatim.
  3. Preflight — Stage 1 flags 11 portable, 1 manual, 0 ineligible; the manual line is confirmed with Bell Canada before its wave ships.
  4. pre-validate per account — three Stage 2 calls (one CSR each) gate to ready: true.
  5. CSV import — Stage 3 uploads the 12 rows once; every valid row is stored as its own request, staged.
  6. Staged dispatch — wave 1 ships the four Frontier numbers, wave 2 (day +2) the Consolidated block, wave 3 the Toronto line after its manual confirmation lands. A CSR mismatch on wave 1 corrects the remaining waves’ CSV before they dispatch.
  7. LOA per wave — each wave’s rows run the Stage 4 upload → sign → submit triple in one operator session, the signer reviewing exactly the numbers in that wave.
  8. Track on events — the Stage 6 board moves per webhook; the Stage 5 list+timeline sweep reconciles nightly.
  9. Cutover per row — as each number.ported fires, the storefront’s routing attaches and the shipment checklist marks the row; the Toronto line gets its dial-and-SMS double-check.
  10. Close — once all 12 rows are completed (or explicitly accounted for), the ledger closes and the losing-account confirmation goes out.
The whole shape: preflight once, import once, stage deliberately, LOA in waves, track by events, cutover per row. The batch does not change the per-number state machine — it changes whose discipline keeps twelve of them from stalling in parallel.

See Also