Tutorial · JavaScript

Validate phone numbers in JavaScript

From a quick regex to numbering-plan validation with carrier and line type: three ways to validate phone numbers in the browser and in Node.js, with code we ran.

Updated October 29, 2026 · 7 min read

Phone number validation in JavaScript works at three levels: a regex checks the shape of the input, libphonenumber-js checks the number against each country’s numbering plan in the browser or in Node.js, and a phone validation API on your server adds the carrier and line type and gives you a result users cannot bypass. This guide shows all three with code we ran: the regex and library snippets with Node.js 26 and libphonenumber-js 1.13.15, the API calls against a local stub of the numlookupapi API that returns the documented responses.

Which check do you need?

CheckCatchesRunsCost
RegexLetters, missing +, too many digitsBrowser, serverFree
libphonenumber-jsWrong length or prefix for the country, impossible numbers; gives the E.164 form and, with full metadata, the number typeBrowser, serverFree, 80 to 145 kB of metadata
Validation APIAll of the above, plus carrier, line type, location and time zones, in a check users cannot skipServerOne request per number

Most sign-up forms use the library while the user types and the API once on submit.

1. Regex: the shape only

A regex is fine as a first filter, for example to reject letters before anything else runs:

const E164 = /^\+[1-9]\d{1,14}$/;
E164.test("+442079460958");    // true
E164.test("+15555555555");     // true, but not a real number
E164.test("+44 20 7946 0958"); // false: spaces are not allowed in E.164

The pattern checks the E.164 format: a plus, a first digit that is not 0, at most 15 digits. It cannot know that UK mobile numbers start with 7 after the country code, that German numbers have different lengths, or that the US area code 555 is not in service. Don’t write a regex per country either: numbering plans change several times a year.

2. libphonenumber-js: numbering-plan validation

libphonenumber-js is a JavaScript port of Google’s libphonenumber with the official metadata for every country.

npm install libphonenumber-js
import { parsePhoneNumberFromString } from "libphonenumber-js/max";

const phone = parsePhoneNumberFromString("07400 123456", "GB");
if (phone?.isValid()) {
  console.log(phone.number);                // "+447400123456" (E.164: store this)
  console.log(phone.formatInternational()); // "+44 7400 123456"
  console.log(phone.formatNational());      // "07400 123456"
  console.log(phone.getType());             // "MOBILE"
  console.log(phone.country);               // "GB"
}

const fake = parsePhoneNumberFromString("+15555555555");
console.log(fake.isPossible(), fake.isValid()); // true false

Metadata size. The README gives about 80 kB for min (the default import), about 145 kB for max and about 95 kB for mobile. Load the library only on pages with a phone field, for example with a dynamic import() when the field gets focus.

Format while typing. AsYouType formats a partial number the way people write it in that country:

import { AsYouType } from "libphonenumber-js/max";

new AsYouType("US").input("2133734"); // "(213) 373-4"

This is the same metadata the free phone number validator and formatter on this site use in your browser.

3. Server side: a phone validation API

Client-side checks are easy to skip (anyone can call your endpoint without your JavaScript), and the bundled metadata says nothing reliable about carriers. On the server, the numlookupapi API checks the number and returns carrier, line type, location, time zones and every format in one request. Get a free API key on the pricing page (250 requests a month for testing and personal projects, no credit card) and put it in an environment variable:

export NUMLOOKUPAPI_KEY="your-api-key"
// numlookup.mjs – Node.js 18 or later (built-in fetch)
const API = "https://api.numlookupapi.com/v1/validate";

export async function validatePhone(number, countryCode) {
  const url = new URL(`${API}/${encodeURIComponent(number)}`);
  if (countryCode) url.searchParams.set("country_code", countryCode);
  const res = await fetch(url, {
    headers: { apikey: process.env.NUMLOOKUPAPI_KEY },
    signal: AbortSignal.timeout(5000),
  });
  const body = await res.json();
  if (res.status === 422) return { valid: false, error: body.message }; // invalid_number, invalid_country_code
  if (!res.ok) throw new Error(`numlookupapi ${res.status}: ${body.error?.code ?? body.message}`);
  return body; // an unknown number is 200 with valid: false
}

validatePhone("+14158586273") returns the documented answer:

{
  "valid": true,
  "number": "14158586273",
  "local_format": "4158586273",
  "international_format": "+14158586273",
  "country_prefix": "+1",
  "country_code": "US",
  "country_name": "United States of America",
  "location": "Novato",
  "carrier": "AT&T Mobility LLC",
  "line_type": "mobile",
  "e164_format": "+14158586273",
  "national_format": "(415) 858-6273",
  "rfc3966_format": "tel:+1-415-858-6273",
  "possible": true,
  "number_type": "landline_or_mobile",
  "timezones": ["America/Los_Angeles"]
}

Three things to know about the answers:

Errors: 401 means a missing or wrong key, 429 a used-up monthly quota or, on the free plan, more than 10 requests a minute. Only successful requests count against your quota.

4. Validate a list in batches

For CRM imports and contact lists, send up to 100 numbers per request. Each number counts as one request, identical entries in the same batch are validated and billed once, and the X-Cost response header shows what a request cost. A number that cannot be read at all gets "error": "invalid_number" in its own entry instead of failing the batch.

export async function validateMany(numbers, countryCode) {
  const unique = [...new Set(numbers)];
  const results = {};
  for (let i = 0; i < unique.length; i += 100) { // up to 100 numbers per request
    const res = await fetch(API, {
      method: "POST",
      headers: { apikey: process.env.NUMLOOKUPAPI_KEY, "Content-Type": "application/json" },
      body: JSON.stringify({ numbers: unique.slice(i, i + 100), ...(countryCode && { country_code: countryCode }) }),
      signal: AbortSignal.timeout(30_000),
    });
    if (!res.ok) throw new Error(`numlookupapi ${res.status}`);
    Object.assign(results, (await res.json()).data); // keyed by the number as sent
  }
  return results;
}

The same number written two ways (+14158586273 and +1 415 858 6273) counts twice, so normalize to E.164 with libphonenumber-js first. Without code, the free bulk phone validator checks a list in your browser, and the batch upload in the numlookupapi dashboard validates CSV, TXT and Excel files with the same fields as the API.

5. Put it together: a sign-up endpoint

The library rejects obvious mistakes for free; the API runs only for numbers that can exist. If the API cannot be reached, the endpoint keeps the format check instead of blocking the sign-up:

// phone-route.mjs – Express 5: check the format for free, then ask the API
import express from "express";
import { parsePhoneNumberFromString } from "libphonenumber-js/max";
import { validatePhone } from "./numlookup.mjs";

const app = express();
app.use(express.json());

app.post("/api/phone", async (req, res) => {
  const parsed = parsePhoneNumberFromString(String(req.body.phone ?? ""), req.body.country);
  if (!parsed?.isValid()) {
    return res.status(422).json({ error: "Please enter a valid phone number." }); // no API request spent
  }
  try {
    const result = await validatePhone(parsed.number);
    if (!result.valid) return res.status(422).json({ error: "This number is not in use." });
    // store result.e164_format; decide what a VoIP number may do in your product
    res.json({ phone: result.e164_format, lineType: result.line_type, voip: result.number_type === "voip" });
  } catch (err) {
    console.error(err.message);
    res.json({ phone: parsed.number, lineType: null, voip: null }); // API unreachable: keep the format check
  }
});

app.listen(3000);

In our run, (415) 858-6273 with country US came back as +14158586273 with line type mobile, while +1 555 555 5555 and hello got a 422 without an API request. In the form itself, <input type="tel" autocomplete="tel"> brings up the phone keypad on mobile devices and lets browsers fill in the number; it does not validate anything, so keep the checks above.

What validation cannot tell you

A valid number exists in the numbering plan; it may still be switched off, disconnected or moved to another network. The carrier field names the network the number range was assigned to, which stays the same after a number is ported. For the live status, an HLR lookup asks the mobile network itself: add hlr=1 to a request on a paid plan to get reachable, ported and current_carrier (see the HLR lookup API). For one-time codes, the only proof that a number belongs to the user is a code they type back.

More languages: validate phone numbers in Python and in PHP.

Frequently asked questions

What is the best way to validate phone numbers in JavaScript?

Use libphonenumber-js for format and numbering-plan checks in the browser, and a phone validation API on the server when you also need the carrier, the line type or a check users cannot bypass. Store the E.164 form the check returns.

Is there a regex that validates all phone numbers?

No. A regex can check the E.164 shape (^\+[1-9]\d{1,14}$), but number lengths and valid prefixes differ by country, so only numbering-plan data can tell whether a number can exist. +15555555555 passes the regex and is not a valid number.

How big is libphonenumber-js?

Its README gives about 80 kB for the default 'min' metadata, about 145 kB for 'max' and about 95 kB for 'mobile'. You need 'max' for getType() (mobile, landline, VoIP); with 'min' it returns undefined for most countries. Load it only on pages with a phone field.

Can JavaScript check whether a phone number is active?

No. Format and numbering-plan checks tell you a number can exist, not that it is switched on or still assigned. That needs a live network query (an HLR lookup), which numlookupapi offers with hlr=1 on paid plans.

How do I validate many phone numbers at once?

POST up to 100 numbers to https://api.numlookupapi.com/v1/validate as {"numbers": [...]}. Each number counts as one request, and identical entries in the same batch are validated and billed once.

Phone number API

Validate numbers like this in your app

numlookupapi checks any phone number worldwide in one request and returns validity, line type, carrier, location and every standard format as JSON.

Read the documentation

250 free requests every month. No credit card required.

GET https://api.numlookupapi.com/v1/validate/+12069220880

{
  "valid": true,
  "number": "12069220880",
  "local_format": "2069220880",
  "international_format": "+12069220880",
  "country_prefix": "+1",
  "country_code": "US",
  "country_name": "United States of America",
  "location": "Seattle",
  "line_type": "landline",
  "e164_format": "+12069220880",
  "national_format": "(206) 922-0880"
}

Keep reading

Start using our phone number API for free today!

Get 250 requests / month for free