← Blog

What is phone number porting (and why it breaks carrier lookups)?

Porting lets a number keep its digits while switching carriers — which is exactly why any offline carrier guess goes stale, and why Boundstone lists carrier and port status as not-performed rather than faking them.

Contents

If you've ever looked up a phone number's carrier from its prefix and gotten the wrong network back, number porting is usually why. This post explains what phone number porting is, why it makes offline carrier data unreliable, and why the honest answer is often "we didn't check that."

What is phone number porting?

Phone number porting is moving a phone number from one carrier to another while keeping the same digits. You leave one network, keep your number, and everything downstream that assumed the number still belongs to the first carrier is now wrong.

Regulators mandate this — Local Number Portability in the US, equivalent rules across the EU, UK, and Australia. It's good for consumers. It's quietly hostile to anyone trying to guess a number's carrier from the number itself.

How a number ever mapped to a carrier

Numbers are allocated in blocks. A national numbering plan hands a range to a carrier, and static datasets — the ones bundled inside libphonenumber and similar libraries — record which block went to whom. Look up the prefix, read the block owner, print a carrier name. Fast, offline, free.

That mapping is an allocation record. It tells you who the block was originally assigned to. It says nothing about who runs the number today.

Why porting breaks offline carrier lookups

The moment a number ports, the allocation record and reality diverge. The prefix still points at the original block owner; the subscriber is on a different network. Any offline lookup — bundled data, a cached CSV, a static prefix table — returns the old carrier with full confidence and no way to know it's stale.

Google's libphonenumber says this plainly in its own docs: its carrier mapper returns the carrier a number was originally allocated to, and is unreliable in regions with number portability. The US is the worst case — porting is routine, and libphonenumber's US carrier data is intentionally sparse for exactly this reason.

An offline carrier "answer" is really a guess with a timestamp you can't see.

What your stdlib and libphonenumber already do

Before reaching for any API, know what you already have. libphonenumber and its ports validate format, identify the region, format to national and E.164, and classify line type from bundled metadata. For a lot of jobs that's the whole task.

import { parsePhoneNumber } from 'libphonenumber-js'

const phone = parsePhoneNumber('+16504472983')
phone.country          // 'US'
phone.getType()        // 'FIXED_LINE_OR_MOBILE'
phone.formatNational() // '(650) 447-2983'
phone.isValid()        // true

Carrier is where it stops being reliable. The full Google port exposes a carrier mapper, and Python's phonenumbers has one too:

import phonenumbers
from phonenumbers import carrier

num = phonenumbers.parse("+16504472983")
print(carrier.name_for_number(num, "en"))
# often empty for US numbers — and where it prints a name,
# that's the original allocation, not the current network

That output is the allocation record. After a port, it's wrong, and nothing in the library can tell you it's wrong.

Why Boundstone reports carrier as not_performed

Boundstone's phone endpoint returns what static data can honestly stand behind, and refuses to fake the rest.

curl -s https://api.boundstone.io/v1/verify/phone \
  -H "Authorization: Bearer bs_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+16504472983"}'
{
  "valid": true,
  "e164": "+16504472983",
  "country": "US",
  "line_type": "fixed_line_or_mobile",
  "national_format": "(650) 447-2983",
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}

Read the not_performed array. carrier_lookup, ported_status, and hlr_liveness are listed there because they can't be answered from bundled data — and porting is the reason. We could print a carrier name off the same static block table libphonenumber uses. We'd rather tell you we didn't. A valid you can trust is one that arrives with an honest list of what we skipped.

The same contract shows up on every phone check, in every language, so you never reverse-engineer confidence from missing fields. Same idea as line type and E.164 normalization: a value is only as useful as your certainty about how it was derived.

When you actually need live carrier and port status

There is exactly one honest way to know a number's current carrier and whether it's ported: query a live source at lookup time — a portability database (LRN in North America) or an HLR dip against the mobile network. That's a paid, per-lookup operation, not a bundled file.

Boundstone does not run that by default. Carrier and HLR liveness are a paid opt-in, priced per lookup at 5 credits with hlr:true and refunded when the network cannot answer; left off, they stay in not_performed rather than being emulated with stale data. If your use case genuinely needs the current carrier — routing SMS, or filtering ported landlines — you need a live dip, and you should treat any offline carrier field, from any vendor, as a historical guess.

You can try the static tier with no key on the free phone validator and read the not_performed list yourself.

The short version

  • Porting moves a number between carriers while keeping the digits, so the numbering-plan prefix no longer identifies the current network.
  • Offline carrier data (libphonenumber, cached tables) returns the original allocation — stale the moment a number ports, with no flag to warn you.
  • Only a live query (portability database / HLR) knows the current carrier and port status.
  • Boundstone returns format, region, and line type from static metadata, and lists carrier_lookup, ported_status, and hlr_liveness under not_performed by default, with the live carrier dip priced per lookup on request (hlr:true).

Frequently asked questions

What does it mean to port a phone number?

Number porting is when you keep your existing phone number while switching to a different carrier. Regulations in many countries require carriers to let subscribers take their number with them, so the digits stay the same but the company actually routing calls and texts changes underneath. That decoupling of a number from the carrier that originally issued it is exactly what makes older carrier data go stale.

Why does porting break carrier lookups?

Many carrier lookups guess the carrier from the number's original prefix or block assignment, which points to whoever was first issued that range rather than who serves the line today. Once a number is ported, that original assignment no longer matches reality, so a lookup can confidently return the wrong carrier. Getting the current carrier right requires a live network query such as an HLR dip or a ported-status lookup, not just reading the number's format.

Does a 'valid' phone result tell me the current carrier?

No, and an honest validator should say so. Boundstone's phone verification checks format, region, and line-type metadata, and every response lists what it did not do: carrier_lookup, ported_status, and hlr_liveness are all reported as not performed. So 'valid' means the number is well-formed and correctly regioned, not that its carrier is current or that the line is live; live carrier, ported-status and reachability data are available as a paid opt-in (hlr:true, 5 credits).

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist