On this page
Phone number validation in code has four rungs: check the syntax, check the number against its country's numbering plan, find the current line type, and confirm that the number is live or reachable on a channel. A libphonenumber port covers the first two for free, in every major language. The last two need network data from an API. This hub explains each rung and links to a tested tutorial per language.
What does "validating a phone number" actually mean?
Developers ask four different questions under one name, and most bugs come from answering the wrong one. The ladder below orders them by cost and certainty:
| Rung | Question | How | Cost | Changes when |
|---|---|---|---|---|
| 1. Syntax | Is it shaped like a phone number? | Normalization, a loose pre-filter | Free, local | Never |
| 2. Numbering plan | Could this number exist in its country? | libphonenumber: isValid | Free, local | Plans change every few weeks |
| 3. Line type and carrier | Is it a mobile, a landline or VoIP today? | Carrier lookup | Paid per conclusive answer | On porting |
| 4. Live status and channels | Is it in service? Does the person use WhatsApp? | Network status, registration checks | Paid per conclusive answer | Any day |
Each rung filters the input for the next one, and each costs more. So run them in order: never pay for rung 3 on a number that fails rung 2. The popular Stack Overflow question "Validate phone number with JavaScript" had over 806,000 views on 2026-09-25, and most of its answers stop at rung 1.
Why is a regex only rung one?
A regex can check shape. It can't know which country codes exist, how long numbers are in each country, or which ranges are allocated. In our test of 11 strings, 8 passed the common E.164 pattern ^\+[1-9]\d{1,14}$ and failed real validation, including +999… (no such country code) and +1 123 456 7890 (no US area code starts with 1).
The details are in E.164 regex: why a pattern is not enough. There's one good use for the pattern: as a database constraint on values you've already normalized, so no code path can store 07911 123456 in an E.164 column.
Which library should you use in each language?
Google's libphonenumber is the reference. Every recommended option below uses its metadata, so the same input gets the same answer, as long as the versions match. Versions were checked on 2026-09-25:
| Language | Library | Licence | Version tracking | Tutorial |
|---|---|---|---|---|
| JavaScript / TypeScript | libphonenumber-js (import /max on the server) | MIT | Own versions; metadata updated frequently | JavaScript and Node |
| Python | phonenumbers (or phonenumberslite) | Apache 2.0 | Same numbers as Google (9.0.40) | Python |
| PHP | giggsey/libphonenumber-for-php (or -lite) | Apache 2.0 | Same numbers as Google (9.0.40) | PHP and Laravel |
| Go | github.com/nyaruka/phonenumbers/v2 | MIT | Own versions; metadata may trail by a few releases | Go |
| Java / Kotlin | com.googlecode.libphonenumber:libphonenumber | Apache 2.0 | The original (9.0.40) | Java and Kotlin |
Two cautions apply to all of them. First, pick the full metadata where there's a choice. libphonenumber-js defaults to "min" metadata, which checks little more than length: +49 1112 3456789 passed isValidPhoneNumber() with min and failed with max in our test. Second, keep the library current. Google's release notes list 25 releases in 2025 and 19 between January and 23 September 2026, and every one updated phone metadata for at least one region.
What's the difference between isPossible and isValid?
It's the most common confusion on rung 2. The Stack Overflow question "isPossibleNumber vs. isValidNumber" has been viewed over 8,000 times.
- isPossible checks only the length for the country. Fast, forgiving, good for "keep typing" hints.
- isValid also checks the leading digits against the ranges the country has allocated. Use it before you store a number or pay for a lookup.
+1 123 456 7890 is the classic example: ten digits is a possible US length, but the number isn't valid. The same names appear in every port (is_possible_number and is_valid_number in Python, IsValidNumber in Go).
What do all the tutorials have in common?
We wrote the five language tutorials to the same pattern, and ran all the code against our API in test mode. The shared rules are worth knowing even if you use another stack:
- Normalize to E.164 once, at the edge. Parse with a default region from a country picker, check validity, store the E.164 string. Keep the raw input separately if support needs it.
- Take the region from the parser.
+44 7911 123456is a Guernsey number (GG), and+1covers more than 20 countries and territories. A dialling-code lookup table gets these wrong. - Validate twice. Client-side for the user's benefit; server-side because it's the only check you can trust.
- Send numbers in POST bodies, never in URLs, where they end up in logs.
- Treat unknown as missing data. A
nullanswer is notfalse. Keep your default and re-check later. - Retry only what the API marks retryable, and wait as long as
Retry-Aftersays.
The E.164 format guide goes deeper on storage and deduplication.
When should you call an API instead of relying on a library?
When the answer depends on the network rather than the numbering plan. A library knows that +1 202 555 0143 is a valid US number of type FIXED_LINE_OR_MOBILE. It can't know which, because US ranges don't separate the two, and it can't know whether the number was ported to a VoIP provider last month. Rung 3 and 4 questions that come up in practice:
- Should we send this OTP by SMS? Line type decides: texts to fixed lines and toll-free numbers fail or cost money for nothing. See OTP fraud prevention checks.
- Is this sign-up using a disposable or VoIP number? See VoIP number detection.
- Is the number still in service? See how to check if a phone number is active.
- Which channel will reach this customer? See checking WhatsApp registration.
How does the MobileValidate API fit the ladder?
The API runs rungs 1 and 2 on every request, for free, with libphonenumber metadata. Numbers that fail come back as invalid_number, repeats as duplicate, and neither is checked. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). Rungs 3 and 4 are the checks you request:
curl https://api.mobilevalidate.com/v1/lookup \
-H "Authorization: Bearer $MOBILEVALIDATE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"numbers": ["+447700900001", "07700 900002", "12345"], "checks": ["carrier"],
"default_country": "GB", "wait": 10}'With a test key, the three rows come back as valid with line_type: "mobile", valid with status unknown (reason NO_DATA, not billed), and invalid_number. Test keys are free and return fixed answers for the test numbers. The quickstart takes about five minutes.
One detail shows up in every tutorial: libphonenumber rejects +44 7700 900xxx, because the UK regulator Ofcom reserves it for drama. Our test numbers come from that range, so each tutorial lets it through locally only when the key starts with mv_test_.
Where should each rung run?
Put each check where it's cheapest and where you can trust it:
| Where | Rungs | Why |
|---|---|---|
| Browser or mobile app | 1–2 | Instant feedback while the user types; format with AsYouType or AsYouTypeFormatter |
| Your API server | 1–2, then 3–4 | The only validation you can trust; the only place an API key may live |
| Background jobs | 3–4 | Slow checks or bulk jobs, re-checks of stored numbers, retries after unknown |
| Database | 1 | A CHECK constraint that stored values are E.164 |
In a sign-up flow, rungs 1 and 2 decide whether the form is accepted. Rungs 3 and 4 add information: use it when it's conclusive, and don't block a real user because a lookup timed out. In batch work the order is the same, but everything runs server-side, and a free estimate first tells you how many rows are invalid or duplicated before you spend anything.
How often should stored numbers be re-validated?
Rung 2 answers change only when a numbering plan changes, so re-running the library after an upgrade is enough, and it's free. Rung 3 and 4 answers age. Numbers get ported, disconnected and reassigned, and people install and delete apps. Store every answer with its checked_at timestamp, and decide per use:
- Before an important message, such as an OTP or a delivery notice: use an answer from the last few days, or check again.
- For list hygiene, such as a marketing list with consent: re-check on a schedule, for example monthly or quarterly.
- After a delivery failure: re-check that number now.
Repeat checks within the freshness window are served from your account's cache and aren't billed again. How to clean a phone number list in bulk shows a full workflow.
How should you test phone validation?
Split tests by rung, because they fail for different reasons:
- Rungs 1–2 (offline, deterministic): one valid number per country you serve, in national and international format; an impossible area code; too short and too long; no region and no
+; a shared country code (+44 7911…→GG). Pin the library version, and expect a few fixtures to change on upgrades. - Rungs 3–4 (network): use test keys and test numbers.
+447700900001returns data,…002and…003returnunknown,…004stays pending for about five seconds, and…429returnsrate_limited. Nothing is billed and no real network is touched. - Never real customer numbers in fixtures. They're personal data, and they change owners.
What are the key takeaways?
- Validation is a ladder: syntax, numbering plan, line type, live status and channels. Run the rungs in order of cost.
- Use a libphonenumber port for rungs 1 and 2, with full metadata, and update it roughly monthly.
isValid, notisPossible, before you store a number or pay for anything.- A regex is a storage constraint, not a validator.
- Line type, porting and reachability need network data from an API, called from your server.
- Unknown is missing data. Retry only retryable errors, and test every branch with free test numbers.
Sources
- libphonenumber — Google, 2026
- libphonenumber release notes — Google, 2026
- ITU-T Recommendation E.164 — International Telecommunication Union, 2026
- Validate phone number with JavaScript (question 4338267) — Stack Overflow, 2026
- isPossibleNumber vs. isValidNumber (question 39036633) — Stack Overflow, 2026
Frequently asked questions
What is the best library for phone number validation?
Google's libphonenumber or a port of it in your language: libphonenumber-js for JavaScript, phonenumbers for Python, giggsey/libphonenumber-for-php for PHP, nyaruka/phonenumbers for Go, and the original library for Java and Kotlin. They share Google's numbering-plan metadata.
Is a validation library enough, or do I need an API?
A library answers whether a number fits its country's numbering plan. It can't tell you the current line type after porting, whether the number is in service, or whether the person uses a messaging app. For those you need a lookup API.
How often should I update my phone validation library?
Monthly is a reasonable default. Google published 25 metadata releases in 2025, roughly one every two weeks, and each changed the numbering data of at least one country.
Where should phone validation run: client or server?
Both, for different reasons. Validate in the browser or app for instant feedback, then validate again on the server, which is the only check you can trust. Paid lookups always run on the server so API keys stay secret.
Related services and guides
More from the blog
All articlesDevelopers
Phone number validation in JavaScript and Node.js
Validate phone numbers in JavaScript with libphonenumber-js: min vs max metadata, E.164, form input, and a live line-type check with runnable Node code.
9 min read
Developers
Phone number validation in Python: phonenumbers, Pydantic and a live check
Validate phone numbers in Python with the phonenumbers library: is_valid vs is_possible, E.164, a Pydantic validator, pytest and a live check with httpx.
7 min read
Developers
Phone number validation in PHP and Laravel
Validate phone numbers in PHP with libphonenumber-for-php, write a Laravel rule, and add a live line-type check with Guzzle that never breaks the form.
7 min read

