Skip to content
zelnum.com
Phone Validation

Phone Number Validation: Possible, Valid, and Reachable Are Not the Same Thing

Short answer: when you ask "is this phone number valid?" you are actually asking three separate questions. Possible means the digits fit the length range of the numbering plan. Valid means the prefix matches a range a regulator has assigned. Reachable means there is a live line on that number right now, and only a network lookup can answer it. A regex answers none of the three, and a true from a validation library only means the library's dataset agrees with the prefix.

Confusing these layers produces two failure modes that look nothing alike. Numbers that pass your check and then bounce forever, and real numbers that your own form rejects. Both cost money, and both become fixable once you know which layer is failing.


What each layer actually proves

Layer The question it answers Typical tool What a false usually means
1. Possible Do the digits fit the plan's length range? Length check, isPossibleNumber() Almost always a parse problem, not a bad number
2. Valid Is the prefix inside an assigned range? Prefix metadata, isValidNumber() Usually a dataset that predates the range
3. Reachable Is a line allocated and in service? HLR or carrier lookup The number really is not in service

The order matters because each layer costs more than the one below it. Layer 1 is arithmetic. Layer 2 is a lookup in a file shipped inside your library. Layer 3 is a network round trip against carrier data, which is the only layer worth paying for.

Layer 1: "possible" is a length check and nothing else

Most engineers carry an assumption that a phone number is 10 digits, or 11 once you add the country code. That assumption holds for the United States and almost nowhere else. Here are the length ranges recorded in the metadata of phonenumbers 9.0.41, read directly from the library:

Country Country code Mobile lengths All lengths in the general descriptor
United States+11010
Canada+1107, 10
United Kingdom+44107, 9, 10
Germany+4910, 114 through 15
France+3399
Italy+399, 106 through 12
Spain+3499
Netherlands+319, 115 through 11
Brazil+5510, 118 through 11
Mexico+521010
India+91108 through 13
Indonesia+629 through 127 through 17
Nigeria+2341010 through 14
Japan+81108 through 17
Singapore+6588, 10, 11
Australia+6195 through 12

Mobile numbers cluster at 9 or 10 digits, but Indonesia spans 9 to 12 and the Netherlands ships both 9 and 11. The general descriptor is wider still, because it also covers service numbers, short codes and corporate ranges. Any pattern that demands a fixed digit count will reject valid numbers the moment your list leaves North America.

This is what isPossibleNumber() does. It counts digits and compares them against these ranges. Nothing about the prefix is consulted, which is why it stays fast and why it can only say "maybe".

Layer 2: "valid" is a prefix lookup against a dataset with a release date

isValidNumber() goes further and checks the prefix against ranges that the library ships as metadata. That metadata is a snapshot taken when the package was built. When a regulator assigns a new range, every installed copy keeps rejecting it until somebody upgrades.

In January 2026 a thread on the libphonenumber discussion group reported three North American area codes, 357, 837 and 679, that isValidNumber() flagged as invalid even though they had been allocated. A Google engineer replied in the same thread that the codes were assigned to their regions but not yet assigned to operators, and that anyone running metadata older than the update would see them fail.

We reproduced it on both sides before writing this, against phonenumbers 9.0.41 in Python and libphonenumber-js 1.13.15 in JavaScript:

// libphonenumber-js 1.13.15
parsePhoneNumber("+14155550123").isValid(); // true  - 415 is an old assignment
parsePhoneNumber("+13575550100").isPossible(); // true
parsePhoneNumber("+13575550100").isValid(); // false - 2025 assignment, missing from metadata
Number isPossible() isValid() Why
+14155550123truetrueAssigned long ago, present in the metadata
+13575550100truefalse2025 allocation, absent from the metadata
+18375550100truefalseSame cause
+16795550100truefalseSame cause
+10000000000truefalseCorrect NANP shape, area code 000 does not exist
+11234567890truefalseCorrect NANP shape, area code 123 is unassigned
+1234567falsefalseBelow the minimum length

Two things follow from that table. A number can be possible and invalid at the same time, which is the whole difference between the two functions. And an invalid verdict is a statement about your dataset version, not about the number.

The most common attempted fix is switching libphonenumber-js from its default metadata to the full libphonenumber-js/max build. It does not help. We ran both. The max metadata adds precise patterns for ranges that already exist in the dataset, so it catches more malformed numbers inside known ranges. It cannot add a range the dataset never had. Upgrading the package is the only thing that clears a false negative caused by a new assignment.

Layer 3: "reachable" is the only layer that talks to the network

Layers 1 and 2 run offline against static data. Layer 3 asks the network itself whether the number is allocated, whether the line is in service, and what kind of line it is. That is the job of an HLR lookup or a carrier lookup, and it is the difference between "this number could exist" and "this number exists on a switch right now".

Line type comes from this layer: mobile, fixed, VoIP, toll-free, pager. It is the field that decides whether an SMS can arrive at all, because a fixed line cannot receive one no matter how valid its prefix is.

The leading-zero trap

National formats carry a trunk prefix in front of the number. International numbers do not. ITU-T E.164 defines the maximum length at 15 digits including the country code, and the trunk prefix is explicitly not part of the international number. Most libraries handle this for you when you parse with the right country context. Italy is the exception.

Country As written nationally National significant number E.164
Germany030 12345630123456+4930123456
United Kingdom0121 234 56781212345678+441212345678
France01 23 45 67 89123456789+33123456789
Italy02 1234 56780212345678+390212345678

Germany, the United Kingdom and France drop the zero. Italy keeps it, because the zero belongs to the subscriber number rather than to the dialling plan. A normaliser that runs digits.replace(/^0/, "") will corrupt every Italian number it touches, and the corrupted value will often still pass a length check afterwards.

Where a regex is the right tool, and where it is not

Regex is useful in exactly one place: cheap input hygiene. It can strip brackets and dashes, catch an obvious typo before a request is sent, and keep junk out of your database. What it cannot do is decide whether a number is real, because that answer does not exist in the string.

What you want to do Right tool Why
Reject obviously mistyped input at the form Character and length check Cheap, forgiving, and does not block real numbers you cannot verify yet
Store numbers in one consistent form Parse to E.164 with a library Handles trunk prefixes and country context that a regex does not know about
Confirm the prefix is assigned Prefix metadata, layer 2 Requires the assigned-range tables, which change every year
Decide whether to spend money on an SMS Network lookup, layer 3 Only this layer knows whether the line can receive a message

Which layer does your use case actually need

Use case Minimum layer Reasoning
Form validation before submit 1 Stop typos, do not reject real numbers. Blocking a sign-up on layer 2 is how you lose customers who own valid numbers in recently opened ranges
Cleaning an imported CRM list 2, then 3 Layer 2 removes the bulk locally for free, then layer 3 is worth paying for on the remainder
Sending SMS or OTP codes 3 A valid fixed line that cannot receive SMS burns credit and reads as a delivery failure downstream
Fraud and risk review 3, plus line type VoIP, pager and non-fixed ranges behave differently from mobile ranges under the same geographies
De-duplicating a database 2 Two spellings of one number collapse into a single record once both are parsed to E.164

What none of these layers can tell you

Layer 3 tells you a line exists. It does not tell you who uses it, whether they want your message, or whether they will answer a call. Push any of these tools past that line and you are guessing.

Number types also move. A fixed line can become a VoIP line after porting, and number portability means the range data that originally described a block does not always describe the network serving it today. A type field is a strong hint with a real error rate, not a fact about the future.

The practical consequence for your logs: record the library version and the lookup timestamp next to every result. When a number you validated six months ago now fails, you need to know whether the number changed or your data did.

Our own API sits at layer 3. It reads allocation, line type and reachability from carrier data, and it returns a reason code for each verdict. It cannot repair a number that was typed wrong, and it cannot tell you whether the person behind the line will respond. Anyone selling you that answer is selling you something else.

Running this in bulk

The ordering that survives real data is: normalise everything to E.164 first, run layer 2 locally to shrink the list, then send only the survivors to a network lookup. Doing it in that order means you pay for layer 3 only on numbers that already passed the free checks.

For a single number, the free phone validator runs all three layers and returns the line type alongside the verdict. For lists, bulk validation methods compared walks through the ordering and the cost trade-offs, the carrier lookup API and HLR lookup API pages document the layer 3 endpoints, and the carrier data guide explains what each returned field means before you route on it.

FAQ

Why does isValidNumber() return false for a real phone number?

Usually because the library's metadata predates the range. New area codes, reopened ranges and new mobile prefixes are rejected until the package is upgraded. In January 2026 three North American codes, 357, 837 and 679, were still failing on current installs, and switching to larger metadata builds did not fix it. Upgrade the library, and log its version next to each result so stale data is visible instead of silent.

Is a regex enough to validate a phone number?

No. A regex can confirm that a string looks like a phone number and strip formatting. It cannot know which prefixes a country has assigned, and it cannot know whether a line exists. Every pattern strict enough to be useful will also reject real numbers from open numbering plans such as Germany or Japan.

What is the difference between isPossibleNumber and isValidNumber?

isPossibleNumber() compares the digit count against the length range for that country. isValidNumber() also checks the prefix against assigned ranges. A number can be possible and invalid at the same time, which is exactly what happens to a correctly shaped number carrying an unassigned area code.

How long can an international phone number be?

ITU-T E.164 caps an international number at 15 digits including the country code. The national part is shorter and its length depends on the country: 9 or 10 digits for most mobile numbers, 8 in Singapore, up to 12 in Indonesia. That is why you cannot validate a national number on digit count alone.

Why do some countries keep a leading zero and others drop it?

The leading zero is a national trunk prefix, and the trunk prefix is not part of the international number. Most countries drop it when converting to E.164. Italy keeps it because the zero belongs to the subscriber number itself. This is why hard-coded zero-stripping damages Italian data while leaving the value looking plausible.

Does phone number validation tell you anything about messaging apps?

No. Messaging presence is a separate check against each platform, and the answer changes from week to week as accounts are created and deleted. Validation answers whether the line exists and what type it is. Those are different questions with different sources, and a tool that claims both in one call is usually bolting two systems together.

Related

About the author
Julian Zhang Principal Verification API Engineer

Julian works on verification API integrations, batch jobs, CSV workflows, task polling, retry handling, result exports, and developer documentation. His guides explain how teams can integrate verification APIs safely and connect results with internal data systems.

View full author profile →
AI answer engines

Request an AI summary of ZelNum

Open ZelNum in your preferred AI assistant to review its bulk WhatsApp, Telegram, phone validation, carrier lookup, social account, crypto exchange, CSV upload, and API workflows.

The prompt points assistants to ZelNum.com, live pricing, product pages, and API documentation.