# Phone number validation in code: libraries, the validation ladder and when to call an API

> The phone validation ladder: syntax, numbering plan, line type and live checks, with the right library for JavaScript, Python, PHP, Go and Java.

Canonical: https://mobilevalidate.com/blog/phone-number-validation-in-code · Last updated: 2026-10-08

![A ladder with four rungs labelled syntax, plan, line type and live, beside five language tabs: JS, PY, PHP, GO and Java.](https://mobilevalidate.com/images/blog/phone-number-validation-in-code.svg)

*Validation is a ladder: syntax, numbering plan, line type, then a live check.*


By MobileValidate team (https://mobilevalidate.com/about) · Published: 2026-10-08 · Category: Developers · Tags: Phone validation, Libphonenumber, E.164, API, Developer tools

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](/services/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"](https://stackoverflow.com/questions/4338267) 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](/blog/e164-regex-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](https://github.com/google/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](/blog/phone-number-validation-javascript) |
| Python | `phonenumbers` (or `phonenumberslite`) | Apache 2.0 | Same numbers as Google (9.0.40) | [Python](/blog/phone-number-validation-python) |
| PHP | `giggsey/libphonenumber-for-php` (or `-lite`) | Apache 2.0 | Same numbers as Google (9.0.40) | [PHP and Laravel](/blog/phone-number-validation-php-laravel) |
| Go | `github.com/nyaruka/phonenumbers/v2` | MIT | Own versions; metadata may trail by a few releases | [Go](/blog/phone-number-validation-go) |
| Java / Kotlin | `com.googlecode.libphonenumber:libphonenumber` | Apache 2.0 | The original (9.0.40) | [Java and Kotlin](/blog/phone-number-validation-java-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](https://github.com/google/libphonenumber/blob/master/release_notes.txt) 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"](https://stackoverflow.com/questions/39036633) 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:

1. **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.
2. **Take the region from the parser.** `+44 7911 123456` is a Guernsey number (`GG`), and `+1` covers more than 20 countries and territories. A dialling-code lookup table gets these wrong.
3. **Validate twice.** Client-side for the user's benefit; server-side because it's the only check you can trust.
4. **Send numbers in POST bodies,** never in URLs, where they end up in logs.
5. **Treat unknown as missing data.** A `null` answer is not `false`. Keep your default and re-check later.
6. **Retry only what the API marks retryable,** and wait as long as `Retry-After` says.

The [E.164 format guide](/blog/e164-phone-number-format-guide-for-developers) 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](/blog/otp-fraud-prevention-checks-before-sending-a-code).
- **Is this sign-up using a disposable or VoIP number?** See [VoIP number detection](/blog/voip-number-detection-for-signups).
- **Is the number still in service?** See [how to check if a phone number is active](/blog/how-to-check-if-a-phone-number-is-active).
- **Which channel will reach this customer?** See [checking WhatsApp registration](/blog/check-if-a-number-is-on-whatsapp-api-guide).

## 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:

```bash
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](/docs/test-mode). The [quickstart](/docs/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](/blog/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. `+447700900001` returns data, `…002` and `…003` return `unknown`, `…004` stays pending for about five seconds, and `…429` returns `rate_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`, not `isPossible`, 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

1. [libphonenumber](https://github.com/google/libphonenumber) — Google, 2026
2. [libphonenumber release notes](https://github.com/google/libphonenumber/blob/master/release_notes.txt) — Google, 2026
3. [ITU-T Recommendation E.164](https://www.itu.int/rec/T-REC-E.164/en) — International Telecommunication Union, 2026
4. [Validate phone number with JavaScript (question 4338267)](https://stackoverflow.com/questions/4338267) — Stack Overflow, 2026
5. [isPossibleNumber vs. isValidNumber (question 39036633)](https://stackoverflow.com/questions/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.
