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 | +1 | 10 | 10 |
| Canada | +1 | 10 | 7, 10 |
| United Kingdom | +44 | 10 | 7, 9, 10 |
| Germany | +49 | 10, 11 | 4 through 15 |
| France | +33 | 9 | 9 |
| Italy | +39 | 9, 10 | 6 through 12 |
| Spain | +34 | 9 | 9 |
| Netherlands | +31 | 9, 11 | 5 through 11 |
| Brazil | +55 | 10, 11 | 8 through 11 |
| Mexico | +52 | 10 | 10 |
| India | +91 | 10 | 8 through 13 |
| Indonesia | +62 | 9 through 12 | 7 through 17 |
| Nigeria | +234 | 10 | 10 through 14 |
| Japan | +81 | 10 | 8 through 17 |
| Singapore | +65 | 8 | 8, 10, 11 |
| Australia | +61 | 9 | 5 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 |
|---|---|---|---|
| +14155550123 | true | true | Assigned long ago, present in the metadata |
| +13575550100 | true | false | 2025 allocation, absent from the metadata |
| +18375550100 | true | false | Same cause |
| +16795550100 | true | false | Same cause |
| +10000000000 | true | false | Correct NANP shape, area code 000 does not exist |
| +11234567890 | true | false | Correct NANP shape, area code 123 is unassigned |
| +1234567 | false | false | Below 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 |
|---|---|---|---|
| Germany | 030 123456 | 30123456 | +4930123456 |
| United Kingdom | 0121 234 5678 | 1212345678 | +441212345678 |
| France | 01 23 45 67 89 | 123456789 | +33123456789 |
| Italy | 02 1234 5678 | 0212345678 | +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
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 →