Porting
Bring an existing number over from another carrier
Porting
If you already own a number with another carrier, port it in instead of buying a new one. Porting supports numbers from the United States, Canada, United Kingdom, Spain, Germany, France, Netherlands, and Australia. The flow is: check portability, upload the required documents, then submit the port-in. Numbers outside the US and Canada additionally need a few country-specific values (an ID copy, proof of address, a tax id, a porting code) collected through the requirements endpoint. Ported numbers arrive voice-ready (and SMS-ready where the order supports messaging), and porting itself is free: the number just starts billing its regular monthly price once it activates.
Coverage varies by number type within a country. Landline and national numbers are portable in every supported country; mobile porting is available for the UK (with a PAC code), while Spanish, German, French, and Dutch mobiles cannot be ported yet. The portability check tells you per number.
1. Check portability
Confirm the number can be moved before collecting anything from the customer. The check returns a results array, one entry per number, each with portable, fastPortable, messagingCapable, and a notPortableReason when it cannot be moved. It also returns the number's countryCode and phoneNumberType (local, mobile, national, toll_free), which you pass to the requirements endpoint for non-US/CA numbers.
const { data } = await zernio.phonenumbers.checkPhoneNumberPortability({
body: { phoneNumbers: ['+34911234567'] }
});
data.results.forEach(r =>
console.log(r.phoneNumber, r.portable, r.countryCode, r.phoneNumberType, r.notPortableReason)
);response = client.phone_numbers.check_phone_number_portability(
phone_numbers=['+34911234567']
)
for r in response.results:
print(r.phone_number, r.portable, r.country_code, r.phone_number_type, r.not_portable_reason)curl -X POST "https://zernio.com/api/v1/phone-numbers/port-in/check" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phoneNumbers": ["+34911234567"]}'2. Country requirements (international ports only)
US and Canadian ports need nothing beyond the LOA, invoice, and account details. For every other country, fetch the extra information the regulator requires with getPhoneNumberPortInRequirements (GET /v1/phone-numbers/port-in/requirements), passing the countryCode and phoneNumberType from the check.
curl "https://zernio.com/api/v1/phone-numbers/port-in/requirements?country=ES&numberType=local" \
-H "Authorization: Bearer YOUR_API_KEY"The response lists fields, each with a requirementId, label, kind, and help text (description, example, sometimes acceptableValues):
kind | How to satisfy it |
|---|---|
text | A string value (e.g. a Spanish CIF, a UK PAC code). |
date | An ISO date string. |
file | Upload the document (step 3) and use the returned documentId as the value. |
address | Nothing to send: the end-user service address from your submit is applied automatically. |
action | Cannot be completed through the API (the response also carries supported: false); contact support to port this number type. |
You will pass the collected values as requirements in the submit. The LOA and invoice also appear as requirements in some countries; those are satisfied automatically by loaDocumentId / invoiceDocumentId, so never send them again here.
3. Upload the documents
A port-in needs a Letter of Authorization (LOA) and a recent invoice from the losing carrier proving ownership, plus any file-kind country requirements from step 2. Upload each with uploadPhoneNumberPortInDocument (POST /v1/phone-numbers/port-in/documents) as multipart form data: a file part (PDF, JPEG, or PNG, up to 10 MB) and a kind part (loa, invoice, or any short slug for requirement documents). Requirement documents are converted to PDF automatically, since regulators reject raw images. Keep the returned documentId for the submit, and upload shortly before submitting: unattached documents are deleted after 30 minutes.
# Upload the LOA (returns { documentId })
curl -X POST "https://zernio.com/api/v1/phone-numbers/port-in/documents" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F file=@loa.pdf \
-F kind=loa
# Repeat for the invoice, and for any file-kind country requirement
curl -X POST "https://zernio.com/api/v1/phone-numbers/port-in/documents" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F file=@passport.pdf \
-F kind=id-copy4. Submit the port-in
Submit the numbers, the LOA and invoice documentIds, the end-user account details in endUser, and (for international ports) the collected requirements. The transfer PIN for US/CA mobile numbers goes in endUser.pinPasscode; international porting codes (like the UK PAC) travel as requirement values instead. Both are forwarded to the carrier and never stored by Zernio.
A few field rules: endUser.countryCode is the service-address country and must be a supported porting country; administrativeArea is required (and validated) for US/CA only; EU business ports can pass taxIdentifier / businessIdentifier. Numbers from different countries must be submitted as separate requests when any of them is outside the US/CA.
const { data } = await zernio.phonenumbers.createPhoneNumberPortIn({
body: {
phoneNumbers: ['+34911234567'],
loaDocumentId: 'DOC_LOA',
invoiceDocumentId: 'DOC_INVOICE',
endUser: {
entityName: 'Acme SL',
authPersonName: 'Jane Doe',
accountNumber: 'A-778812',
streetAddress: 'Calle Gran Via 1',
locality: 'Madrid',
postalCode: '28013',
countryCode: 'ES',
taxIdentifier: 'B12345674',
},
requirements: [
{ requirementTypeId: 'REQ_CIF', fieldValue: 'B12345674' },
{ requirementTypeId: 'REQ_ID_COPY', fieldValue: 'DOC_PASSPORT' },
],
}
});
data.orders.forEach(o => console.log(o.id, o.status, o.error ?? 'ok'));response = client.phone_numbers.create_phone_number_port_in(
phone_numbers=['+34911234567'],
loa_document_id='DOC_LOA',
invoice_document_id='DOC_INVOICE',
end_user={
'entityName': 'Acme SL',
'authPersonName': 'Jane Doe',
'accountNumber': 'A-778812',
'streetAddress': 'Calle Gran Via 1',
'locality': 'Madrid',
'postalCode': '28013',
'countryCode': 'ES',
'taxIdentifier': 'B12345674',
},
requirements=[
{'requirementTypeId': 'REQ_CIF', 'fieldValue': 'B12345674'},
{'requirementTypeId': 'REQ_ID_COPY', 'fieldValue': 'DOC_PASSPORT'},
],
)
for o in response.orders:
print(o.id, o.status, o.error or 'ok')curl -X POST "https://zernio.com/api/v1/phone-numbers/port-in" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumbers": ["+34911234567"],
"loaDocumentId": "DOC_LOA",
"invoiceDocumentId": "DOC_INVOICE",
"endUser": {
"entityName": "Acme SL",
"authPersonName": "Jane Doe",
"accountNumber": "A-778812",
"streetAddress": "Calle Gran Via 1",
"locality": "Madrid",
"postalCode": "28013",
"countryCode": "ES",
"taxIdentifier": "B12345674"
},
"requirements": [
{"requirementTypeId": "REQ_CIF", "fieldValue": "B12345674"},
{"requirementTypeId": "REQ_ID_COPY", "fieldValue": "DOC_PASSPORT"}
]
}'The carrier may split your numbers into several orders (by country, number type, or losing carrier). The response orders[] carries per-order results. A partial failure still returns 201: the failed orders come back with their error set and stay as cancellable drafts, so you can fix and resubmit just those. If a required country value is missing, the order is kept as a draft and the error names exactly what is needed (for example Additional information required: Spanish CIF …); inspect an order's live gaps any time with getPhoneNumberPortInOrderRequirements (GET /v1/phone-numbers/port-in/{id}/requirements).
Track and cancel
List your in-flight port-ins with listPhoneNumberPortIns (GET /v1/phone-numbers/port-in). US/CA ports typically complete in a few business days (same-day for FastPort-eligible numbers); international ports can take several weeks while the carrier and local regulator review the documents. An order moves through these statuses:
status | Meaning |
|---|---|
draft | Created but not accepted by the carrier — including split orders that failed at submit (their error says why). Fix and resubmit, or cancel; drafts abandoned for 14 days are cancelled automatically. |
pending | Submitted; the losing carrier is processing the transfer. |
foc_confirmed | The carrier confirmed the transfer date (focDatetimeActual, the Firm Order Commitment). The number moves on that date. |
ported | Transfer complete. The number is on your account, voice-ready. |
exception | The losing carrier flagged a problem — declineReason says what. Not terminal: the order stays open and returns to pending once the flagged details are corrected and resubmitted. |
cancelled | The order was cancelled and won't proceed. |
Cancel a draft or pending order with cancelPhoneNumberPortIn (DELETE /v1/phone-numbers/port-in/{id}). Once an order reaches ported, the number appears in your regular number list, voice-ready, and starts billing at its country's regular monthly price.