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

Run these in order. Each step protects the next: preflight gates the CSV, the CSV gates staging, staging gates the LOA wave.
  1. Build the E.164 list — one number per line, + prefix, copied from the losing carrier’s inventory rather than retyped.
  2. Pull the CSR per account — a batch across N losing-carrier accounts needs N CSRs. Guessed fields are the #1 rejection class; NAME_MISMATCH and ADDRESS_MISMATCH trace to retyped CSR data, and in a batch they multiply.
  3. Preflight the whole list before importing anything — the dashboard’s Pre-flight portability review card on the Number Porting page runs POST /numbers/porting/check/bulk against the pasted list and renders a per-number verdict plus a summary rollup: portable (carrier-confirmed eligible), ineligible (carrier rejects, reasons listed), and need manual check (the carrier has no live eligibility API, or the check was skipped). Drop ineligible and unresolved-manual rows from the shipment; a non-portable DID discovered now costs nothing, discovered after the LOA costs the carrier’s review window. The API equivalent returns the same summary block (total / eligible / ineligible / not_supported / skipped) for pipeline use.
  4. Lint the CSR fields with pre-validate — the combined gate runs the portability check AND statically checks the CSR fields; gate on ready: true. In a batch this is where an account-number transposition across thirty rows surfaces before it submits.
  5. Build the CSV — header row with exactly these columns (order irrelevant, case-insensitive):
Quote any field containing a comma (billing_address almost always needs it). A header missing a required column aborts the whole upload with 422. 1000 rows per request, 2MB total — split a larger estate across uploads rather than retrying a timed-out one. 6. Import the CSVPOST /numbers/port-in/bulk-csv with the file as multipart. Each valid row becomes its own stored request; duplicate submissions against an active in-flight request are skipped and reported (duplicates_skipped, overall_result: "partial" when some rows failed). Read the validation_errors array per row. 7. Stage the carrier handoff — the rows are stored, status: submitted, nothing dispatched. This is the deliberate gap: file the LOA per row (upload → sign → submit) on the schedule you actually control. A 12-number retail migration is exactly the case for staging — dispatch 3–4 numbers per wave so a CSR mismatch on wave one corrects the CSV before wave two ships, instead of rejecting twelve rows in parallel. 8. Drive the LOA wave per row — the same three-step lifecycle per request: upload → sign → submit. In a batch, the discipline that matters is per-row: one LOA must list every number in that request (a multi-number create) or that one number (a CSV-imported row), signed within 30 days, legible at ≥200 dpi. Do not park a staged row. A stored request that never gets its LOA never reaches the carrier, never gets an FOC, and ages silently — the dashboard flags exactly this state as “draft — LOA pending” for wizard drafts, and the discipline applies to CSV-staged rows the same way.

4. Track the shipment — webhooks first, polling as reconciliation

A 12-number shipment is twelve state machines. The pattern that keeps you out of a poll loop:
  • Webhooks drive the board. Subscribe one endpoint to the porting events — number.ported, porting.request.loa_signed, porting.request.supplement_submitted, porting.request.manual_review_required, porting.request.cancelled — and let each transition move a row on your ops board. Delivery is at-least-once; dedupe on the event id.
  • A daily sweep reconciles. GET /numbers/porting (list) plus GET /numbers/porting/:id/timeline per row is the reconciliation pass — anything a webhook delivery missed surfaces here. The timeline’s per-stage view answers “where is row N stuck” without a carrier call.
  • Poll refresh only per-row, on demand. POST /numbers/porting/:id/refresh re-polls the provider for one row; it is a targeted operator action, not a scheduling primitive. A cron that fans out refresh over a 500-row batch is a carrier call per page load — the events-plus-sweep pattern is the one that scales.
The dashboard renders the same timeline and rejection explanation per request, so the split that works in practice is: events feed your own tooling, the dashboard is the operator’s live board, and the API list+timeline is the nightly reconciliation. All three read the same underlying requests — submit by API, watch in the dashboard.

5. 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: a retail chain’s twelve regional lines

Putting the sections together on the example in the brief — a retailer porting 12 regional storefront numbers off three losing-carrier accounts:
  1. Build the list — 12 E.164 lines, 11 US, 1 CAN; the CAN line goes on the manual-verification plan from step one (DIDWW territory, check-not-supported expected).
  2. Pull three CSRs — one per losing-carrier account; each storefront’s billing address is copied verbatim.
  3. Preflight the 12 — the dashboard’s pre-flight review card flags 11 portable, 1 manual (the CAN line), 0 ineligible; the CAN line is manually confirmed with its carrier before the CSV ships.
  4. pre-validate per account — three calls (one CSR each) gate to ready: true.
  5. CSV import — 12 rows across three account-column values, uploaded once; all 12 rows stored as individual requests, staged.
  6. Staged dispatch — wave 1 dispatches the four numbers on the cleanest account; wave 2 (day +2) the next account; wave 3 the CAN 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 get upload → sign → submit in one operator session, with the signer reviewing exactly the numbers in that wave.
  8. Track on events — the ops board moves per webhook; a nightly list+timeline sweep reconciles.
  9. Cutover per row — as each number.ported fires, the storefront’s routing attaches and the shipment checklist marks the row; the CAN 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