KeepMyNumber
Home Sign in

Port-in orders

A port-in order transfers numbers from their current carrier onto the platform. Orders are asynchronous: after submission, progress depends on the losing carrier and can take from hours (US FastPort-style) to weeks (international/local processes).

Lifecycle

draft ──submit──▶ in_review ──▶ submitted ──▶ in_process ──▶ foc_scheduled ──▶ activating ──▶ completed
                      │              │             │                                          completed_partial
                      └───────── rejection ◀───────┘    fix requirements, resubmit ──▶ in_review
Status Meaning
draft Created; fulfill the requirements, then submit
in_review Submitted to us — our porting desk verifies every field and document before anything reaches the losing carrier
submitted Review passed; sent to the provider, being validated on their side
in_process The losing carrier is processing the port
rejection Something was refused but is fixable — check the requirements' exception_reason, fix, resubmit
foc_scheduled The port date (FOC — Firm Order Commitment) is confirmed; see foc_actual
activating Numbers are being activated on the platform
completed / completed_partial Done — numbers are live (partial: some numbers failed)
cancel_pending Cancellation requested, awaiting provider confirmation
cancelled Terminal. closed_by tells you who ended it: customer (you cancelled), provider (final refusal by the carrier), or ops

The review step is why porting through the platform is safe: a human checks your submission and rejects individual fields with a reason (a porting_order.requirement_updated webhook plus a rejection status) instead of letting the carrier bounce the whole order days later. While the order is in_review, cancelling is instant — nothing has left the platform. forwarded_at on the order tells you when it actually went to the carrier.

How long the review takes depends on your account's standing. New accounts start in manual review: our porting desk verifies every submission (typically during your first month) before forwarding it. Established accounts are switched to auto-forward — a successful submit goes to the losing carrier immediately and you'll see the order move in_review → submitted in the same instant. The flow, statuses and webhooks are identical either way; only the review dwell time changes.

Every transition emits a porting_order.status_changed webhook.

The flow: open an order, fill it in, submit

You manage the order like a shopping cart: open it, add data and documents in any order over any time span, and submit when everything is in place. You never write, render, print or upload an LOA — show the end user the LOA text we give you, collect their drawn signature as a PNG, and send that:

1. POST  /v2/portability_checks                       can this number port?
2. POST  /v2/porting_orders                           open draft order(s), requirements attached
3. PATCH /v2/porting_orders/{id}/requirements/{slug}  fulfill each requirement (e.g. …/recent_invoice)
4. GET   /v2/porting_orders/{id}/readiness            anything still missing?
5. POST  /v2/porting_orders/{id}/submit               hand it to our review desk (in_review)

Step 1 — check the number. Send the raw numbers; get back whether each is portable, its country, number type, and current carrier:

curl -X POST https://api.example.com/v2/portability_checks \
  -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"phone_numbers": ["+447912345678"]}'

Step 2 — open the order. The phone numbers are all it takes — a JSON POST /v2/porting_orders creates draft orders with the route's requirements already attached (snapshotted from the per-country catalog). Supports the Idempotency-Key header:

curl -X POST https://api.example.com/v2/porting_orders \
  -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"phone_numbers": ["+447912345678"], "customer_reference": "crm-1042"}'

The response is an array of orders (see splitting below), each carrying its requirements with per-field status — that array is the complete checklist: account holder name, service address, account number, documents, the LOA. You can also preview the requirements before creating anything — just ask for the number itself: GET /v2/porting/requirements?phone_number=%2B447912345678 (or for a route: ?country_code=GB&number_type=mobile).

Step 3 — fulfill the requirements, one PATCH each, in any order (LOA last) — see the Requirements section below for the shapes (text value, address object, document id, LOA signature). The order stays a draft between calls; order-level settings (FOC date, reference, webhook override) can be edited anytime with PATCH /v2/porting_orders/{id}.

Step 4 — check readiness. A dry run of the submit validation, so your UI can show exactly what is still missing without triggering a failed submit:

curl "https://api.example.com/v2/porting_orders/{id}/readiness" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
{
  "order_id": "8b2f1747-…",
  "status": "draft",
  "ready": false,
  "errors": [],
  "requirements": [
    {"slug": "loa", "fulfilled": true, "problem": null, "...": "..."},
    {"slug": "recent_invoice", "fulfilled": false, "problem": "not fulfilled", "...": "..."}
  ]
}

ready: true means POST …/submit will pass local validation right now.

Step 5 — submit. Validated locally first (same checks as readiness); a 422 lists exactly what is missing and nothing is sent:

curl -X POST "https://api.example.com/v2/porting_orders/{id}/submit" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"

From here everything is asynchronous: watch porting_order.status_changed webhooks (or poll GET /v2/porting_orders/{id}) as the order moves through in_process → foc_scheduled → activating → completed, at which point the number is live in your registry (GET /v2/numbers).

Deleting a draft

Changed your mind before submitting? Delete the draft — it disappears along with its registry reservations, so the numbers can be ordered again immediately. Uploaded documents stay in your library (they are reusable):

curl -X DELETE "https://api.example.com/v2/porting_orders/{id}" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"

Only draft orders can be deleted (409 otherwise). Anything already submitted must go through cancellation instead — the provider side has to be unwound.

Order splitting

One POST /v2/porting_orders may return several orders. Numbers are grouped by country and number type. All orders from one request share a batch_id, and a porting_order.split event tells you it happened. Each order is fulfilled and submitted independently — always iterate over the response array.

Providers may split further (e.g. by losing carrier). That happens when our review desk forwards your order to the carrier — not when you create it — so the sibling orders appear in the batch already submitted, with every requirement, document and approval inherited from the order you fulfilled. A second porting_order.split event (same batch_id, listing the new ids) is sent when it happens; after that, each sibling reports its own status changes. Filter GET /v2/porting_orders?batch_id=… to see all of them.

Requirements

Each order carries requirements — one array with everything you need to provide: the universal end-user fields (auth_person_name, entity_name, billing_phone_number, location) plus the documents and data legally required for its country, number type, and provider. Check them before ordering — pass the number itself (we classify it and answer for its route; phone_number in the response echoes the normalized number), or ask for a route by country_code + number_type:

curl "https://api.example.com/v2/porting/requirements?phone_number=%2B447912345678" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
# or by route:
curl "https://api.example.com/v2/porting/requirements?country_code=GB&number_type=mobile" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"

Requirement statuses: requirement-info-pending → you must act; requirement-info-under-review → we are validating; requirement-info-exception → refused, see exception_reason, fix and resubmit; approved → done.

Requirements are addressed by their slug — recent_invoice on a GB order always means the same thing, so there is no per-requirement id to keep track of. How you fulfill one depends on its field_type:

  • textual — PATCH the value;
  • address — PATCH an address object (street_address and country_code required);
  • document — PATCH the file itself (multipart), or PATCH the id of a document you already uploaded via POST /v2/documents;
  • signature (the LOA) — PATCH the end user's drawn signature as a base64 PNG. The platform does the rest.
# textual
curl -X PATCH https://api.example.com/v2/porting_orders/{order_id}/requirements/account_number \
  -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"field_value": "AB1234567"}'

# address (the service address)
curl -X PATCH https://api.example.com/v2/porting_orders/{order_id}/requirements/location \
  -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"field_value": {"street_address": "1 High Street", "locality": "London",
       "postal_code": "SW1A 1AA", "country_code": "GB"}}'

# document — one call: upload the file straight to the requirement
curl -X PATCH https://api.example.com/v2/porting_orders/{order_id}/requirements/recent_invoice \
  -H "Authorization: Bearer pk_live_YOUR_KEY" \
  -F "file=@invoice.pdf" -F "document_date=2026-08-01T00:00:00Z"

# signature (LOA)
curl -X PATCH https://api.example.com/v2/porting_orders/{order_id}/requirements/loa \
  -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \
  -d '{"signature": "data:image/png;base64,iVBORw0KGgo...",
       "locale": "es", "accepted_disclosures": ["authorised", "cease_service"]}'

The direct upload stores the file in your document library like any other upload (it gets an id, appears in GET /v2/documents, is downloadable). Prefer the two-step flow — POST /v2/documents first, then PATCH the returned id — when one file serves several orders (e.g. after a split): upload once, attach everywhere.

Invoices and LOAs must usually be fresher than 30 days — we validate this at submit time so you fail fast instead of waiting days for a carrier rejection.

The LOA (Letter of Authorization)

The platform hosts the LOA end-to-end — you never write LOA wording, never render a PDF, and never handle print/scan/upload. Your only job: show the text, collect a drawn signature.

  1. Get the text. The localized LOA wording ships in two places, so both flows have it when they need it:
  2. route preview: GET /v2/porting/requirements?country_code=GB → loa (placeholders like {{account_holder}} unresolved — no order yet);
  3. on the order: every field_type: "signature" requirement on GET /v2/porting_orders/{id} carries a loa block with placeholders already filled from the order's other requirements. Anything still shown as {{…}} tells you which requirement is still unfilled.

Each block has text keyed by language (en, es, fr, de, ar on the built-in templates), the disclosures to show as checkboxes, and a signature_spec describing the PNG constraints (min 200×60 px, ≤ 512 KB).

  1. Collect the signature — render the text in the end user's language, let them draw a signature (canvas → PNG), and PATCH it onto the requirement:

    bash curl -X PATCH ".../v2/porting_orders/{order_id}/requirements/loa" \ -H "Authorization: Bearer pk_live_YOUR_KEY" -H "Content-Type: application/json" \ -d '{"signature": "data:image/png;base64,…", "locale": "es", "signer_name": "Jane Doe", "signer_email": "jane@acme.example"}'

signer_name defaults to the order's auth_person_name requirement and locale (the language you displayed) defaults to en.

The platform validates the PNG, renders the signed LOA PDF from the order's other requirements, appends a Certificate of Completion page (signer name, timestamp, IP, user agent, template version, language shown, SHA-256), stores it as a Document (its id lands in the requirement's field_value), and emits a loa.signed event.

  1. Audit lookup whenever needed (regulatory disputes, ops investigations) — and the signed PDF is downloadable like any document:

    bash curl https://api.example.com/v2/porting/loa/signatures/{document_id} \ -H "Authorization: Bearer pk_live_YOUR_KEY" curl https://api.example.com/v2/documents/{document_id}/download \ -H "Authorization: Bearer pk_live_YOUR_KEY" -o signed_loa.pdf

Two things to know:

  • One signature is enough. Editing a field after the LOA is signed does not invalidate the signature or block submission — fix the data and resubmit. Our review desk checks every order before it reaches the carrier; only if it judges a change really requires a fresh signature will it flag the loa requirement as an exception (with the reason). Signing last is still the tidiest habit: the PDF then shows the final values.
  • Disclosures default to accepted. Omitting accepted_disclosures means the signature covers all required disclosures (they are printed above the signature line on the PDF, like a paper LOA). Pass the list explicitly if your UI tracks each checkbox.

End-user data

End-user data must match the losing carrier's records exactly — mismatches are the second most common rejection cause. It is ordinary requirements, on every order: auth_person_name (mandatory), location (mandatory, an address object), entity_name and billing_phone_number (optional), plus route-specific ones like account_number or port_out_pin. Fill each like any other requirement — PATCH …/requirements/auth_person_name, PATCH …/requirements/location. There is no separate "end user object" to manage: the requirements array is the single place these facts live, and the platform assembles what the carrier and the LOA need from it.

Rejections

If the order hits rejection, the problem is fixable — nothing is lost:

  1. GET /v2/porting_orders/{id} — look for requirements in status requirement-info-exception; each carries an exception_reason explaining exactly what was refused. Notes from our operations team and the provider appear on the order in the dashboard.
  2. Fix the data or documents (only the flagged ones — everything else stays attached), then POST …/submit again. The signed LOA stays valid across these fixes — you never have to re-sign just because a field changed (our desk will flag the loa requirement explicitly in the rare case a fresh signature is genuinely required).

If the carrier refuses the port definitively, the order goes straight to cancelled with closed_by: "provider" — that one is terminal; create a new order if you want to retry from scratch.

Cancellation

POST /v2/porting_orders/{id}/cancel. After submission the order enters cancel_pending — cancellation is not guaranteed once the losing carrier has confirmed the port; watch for the final cancelled (or a return to in_process) via webhook. A never-submitted draft doesn't need cancelling — delete it instead.

Duplicates and idempotency

  • A number that is already active or in an active port on the platform is refused with 409 and the existing order ids.
  • Send Idempotency-Key on creation: retries with the same key and body return the original response instead of creating duplicate orders.