# Phone number validation in JavaScript: regex, libphonenumber, and E.164

_2026-07-18 · Boundstone (https://boundstone.io/blog/validate-phone-javascript)_


Phone validation looks easy until the second country shows up. A US number is ten digits; a UK mobile is eleven with a leading zero you drop for international; an Australian mobile starts +61 4. No single regex survives contact with the real world's numbering plans. Here's how to validate phone numbers in JavaScript in a way that does — and a candid note on when you actually need an API for it.

## The regex, and why it breaks

Here's the kind of pattern people reach for:

```javascript
const US_PHONE = /^\+?1?\s*\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}$/;
US_PHONE.test("+1 650 447 2983"); // true
```

That's fine if you only ever take US numbers and you're mostly guarding against typos. The moment you accept international input it falls apart: it doesn't know that `+44 7911 123456` is a valid UK mobile, that `+61 491 570 156` is a valid Australian one, or that `+1 555 555 5555` is *shaped* right but sits in an unassigned range. Numbering plans encode length rules, prefix rules, and mobile-vs-fixed rules that differ per country and change over time. A regex can't carry that, and the "full international phone regex" you'll find is both enormous and wrong.

## libphonenumber-js: the approach that holds up

Google maintains **libphonenumber**, the library that ships inside Android to parse and validate numbers using the actual per-country rules. For JavaScript, [`libphonenumber-js`](https://www.npmjs.com/package/libphonenumber-js) is a compact port:

```bash
npm install libphonenumber-js
```

```javascript
import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js";

isValidPhoneNumber("+16504472983");        // true
isValidPhoneNumber("+1 555 555 5555");     // false — right shape, unassigned

const phone = parsePhoneNumber("+16504472983");
phone.country;            // "US"
phone.number;             // "+16504472983"  (E.164 — store this)
phone.getType();          // "FIXED_LINE_OR_MOBILE"
phone.formatNational();   // "(650) 447-2983"
phone.formatInternational(); // "+1 650 447 2983"
```

If your input doesn't include a country code, pass a default country so the national number can be interpreted:

```javascript
isValidPhoneNumber("07911 123456", "GB"); // true
```

Two things worth internalizing. First, always store `phone.number` — the [E.164](/blog/what-is-e164) form. It's the unambiguous international format every downstream API (SMS, voice, CRM) expects. Second, `getType()` is honest about ambiguity: for many numbers it returns `FIXED_LINE_OR_MOBILE`, because the numbering plan genuinely can't distinguish the two from the digits alone. That's not a bug — it's the library refusing to guess, which is exactly what you want.

## What metadata can't tell you

libphonenumber works on *metadata* — the published structure of numbering plans. It can tell you a number is valid, which country it belongs to, and how to format it. It cannot tell you:

- **which carrier** currently serves the number (numbers get ported between carriers, so the prefix no longer settles it),
- **whether it's still assigned to a live handset**, or
- **whether that handset is switched on right now** (a live "HLR" lookup).

Those need a network query, not a lookup table. This is the honest dividing line, and it's worth being clear-eyed about: **for validation and formatting, libphonenumber-js in the browser or in Node is genuinely all you need, and you don't need us for it.**

## So where does an API come in?

Three honest reasons, and we'd rather name them plainly than pretend the metadata layer is a moat:

1. **One consistent contract across languages and clients.** If you validate phone numbers in a browser, a Node service, and a Python worker, you're maintaining three copies of the metadata and three definitions of "valid." One API call gives every surface the same answer.
2. **Phone, email, and IP in one system.** Signup screening usually wants all three. Doing them through one credit system with one honesty contract beats stitching three libraries together.
3. **The layer past metadata, when you want it.** Carrier, ported-status and live-reachability (HLR) checks are the things libphonenumber cannot do. On Boundstone they are marked `not_performed` by default and run as a paid opt-in — send `hlr:true` for a real network dip at 5 credits, refunded when the network cannot answer. Never faked either way.

Here's the call, and note what comes back:

```javascript
const res = await fetch("https://api.boundstone.io/v1/verify/phone", {
  method: "POST",
  headers: { authorization: "Bearer bs_live_YOUR_KEY", "content-type": "application/json" },
  body: JSON.stringify({ phone: "+16504472983" }),
});
const data = await res.json();

console.log(data.valid, data.country, data.line_type, data.e164);

console.log("checked:    ", data.checks.performed);
// ['format', 'region', 'line_type_metadata']

console.log("not checked:", data.checks.not_performed);
// ['carrier_lookup', 'ported_status', 'hlr_liveness']
```

That `not_performed` list is the whole point. It says, in the response itself, that today's answer is metadata-grade — the same class of data libphonenumber gives you — and that carrier and liveness were *not* consulted. A phone API that returns a bare `valid: true` and won't tell you whether a live network was ever queried is asking you to trust a word. Prefer the one that shows its work. You can try it with no key on the [phone validator](/tools/phone-validator).

## The short version

Use a regex only to catch typos in a single-country field. For anything real, use `libphonenumber-js` — it's the correct, honest answer for validation and formatting, and it runs client-side for free. Reach for an API when you want one contract across many clients, phone alongside email and IP, or the carrier/liveness layer that metadata can't reach — and only ever trust a "valid" that tells you what it checked.
