Guide · Node.js · SDK · REST API

Phone Number Validation in Node.js — E.164 & Line Type Detection

Phone numbers look simple but come in wildly different formats across 200+ countries. Here's why regex fails, what E.164 normalization means, and how to validate any phone number in a single API call.

1. The problem with phone number validation

Phone numbers are everywhere in modern applications. You need them for SMS verification, two-factor authentication (2FA), delivery notifications, customer support callbacks, and marketing campaigns. A bad phone number means an undeliverable OTP, a failed 2FA flow, or a wasted SMS that costs you money.

The problem is that phone number formats vary dramatically across more than 200 countries and territories. A valid mobile number in Poland looks nothing like one in Japan or Brazil. Users enter numbers with spaces, dashes, parentheses, leading zeroes, country codes, or none of the above. They paste from contacts, type from memory, or copy from a website with arbitrary formatting.

On top of format differences, there are line types to consider. A number might be a mobile line, a landline, a VOIP number, a toll-free line, or a premium-rate number. If you're sending SMS for 2FA, you need a mobile number — sending an OTP to a landline won't work. If you're running a call center, you want to detect premium-rate numbers before dialing them and incurring charges.


2. The naive approach: regex (and why it fails)

The first instinct is to reach for a regular expression. Something like this:

// A common (but broken) phone number regex
const phoneRegex = /^\+?1?\d{10}$/;

function isValidPhone(phone) {
  return phoneRegex.test(phone.replace(/[\s\-().]/g, ''));
}

// Works for US numbers:
isValidPhone('+1 (555) 123-4567');  // true ✓
isValidPhone('5551234567');          // true ✓

// Fails for valid numbers from other countries:
isValidPhone('+48 600 123 456');     // false ✗  (Polish mobile — 9 digits)
isValidPhone('+44 7911 123456');     // false ✗  (UK mobile — 10 digits)
isValidPhone('+91 98765 43210');     // false ✗  (Indian mobile — 10 digits)
isValidPhone('+81 90 1234 5678');    // false ✗  (Japanese mobile — 10 digits)
isValidPhone('+49 170 1234567');     // false ✗  (German mobile — 10-11 digits)
isValidPhone('+55 11 91234 5678');   // false ✗  (Brazilian mobile — 11 digits)

This regex assumes all phone numbers are 10 digits long with an optional US country code. It immediately rejects valid numbers from most of the world. You might try to fix it by allowing a wider digit range:

// "Fixed" regex — accepts 7 to 15 digits
const betterRegex = /^\+?\d{7,15}$/;

// Now it accepts too much:
betterRegex.test('1234567');          // true — but is this a real number?
betterRegex.test('999999999999999');  // true — 15 random digits
betterRegex.test('+00 000 000 000');  // true after stripping — no such country

// And still can't tell you:
// - Is it a mobile or landline?
// - What country does it belong to?
// - What's the E.164 normalized form?
// - Is the number length correct for its country?

The fundamental issue is that a regex can only check character patterns. It cannot verify that the country code exists, that the number length is correct for that specific country, or that the number plan allocation is valid. Regex also tells you nothing about the line type — you cannot distinguish a mobile number from a landline, VOIP, or toll-free number with pattern matching alone.


3. Why phone validation is genuinely hard

200+ country formats

Every country has its own numbering plan with specific rules for digit lengths, area codes, and number prefixes. The ITU E.164 standard allows up to 15 digits total (including country code), but the actual valid lengths vary per country.

CountryCodeExample (E.164)National formatDigits
United States+1+12125551234(212) 555-123410
United Kingdom+44+44791112345607911 12345610-11
Germany+49+4917012345670170 123456710-11
Poland+48+48600123456600 123 4569
Japan+81+819012345678090-1234-567810
Brazil+55+5511912345678(11) 91234-567810-11
India+91+91987654321009876 54321010
Australia+61+614123456780412 345 6789

Variable lengths (7-15 digits)

Phone number lengths range from 7 digits (some Pacific island nations) to 15 digits (the E.164 maximum). Even within a single country, lengths can vary — German mobile numbers can be 10 or 11 digits, and UK numbers range from 9 to 10 digits depending on the area code. A hardcoded length check will either be too strict (rejecting valid numbers) or too loose (accepting garbage).

Line type matters

Phone numbers are assigned to different line types, and the type determines what you can do with the number:

MOBILECan receive SMS — good for 2FA and OTPs
FIXED_LINELandline — cannot receive SMS in most countries
VOIPInternet-based — may or may not receive SMS
TOLL_FREEFree to call — typically business inbound lines
PREMIUM_RATECharges the caller — watch out for fraud
UNKNOWNType cannot be determined from the number plan

E.164 normalization

E.164 is the international standard for phone number formatting: a + followed by the country code and subscriber number, with no spaces, dashes, or parentheses. For example, +48600123456. Storing numbers in E.164 format is essential for interoperability — it's what Twilio, AWS SNS, and every major SMS gateway expects. But converting user input to E.164 requires knowing the country code and stripping the national prefix (the leading 0 in many countries).

National vs. international format

Users in Poland type 600 123 456 (national format), but the international format is +48 600 123 456. In the UK, users dial 07911 123456 locally, which becomes +44 7911 123456 internationally (the leading 0 is dropped). In the US, users type (212) 555-1234, which becomes +1 212 555 1234. Your validation needs to handle both formats.


4. The right solution: one API call

The IsValid Phone API validates phone numbers from 200+ countries in a single GET request. It handles format normalization, country detection, length validation, and line type detection — and returns the number in E.164, national, and international formats.

Format validation
Country-specific length and prefix rules for 200+ countries
Line type detection
MOBILE, FIXED_LINE, VOIP, TOLL_FREE, PREMIUM_RATE, and more
E.164 normalization
Returns the canonical +CC format for SMS gateways and storage
Country detection
Identifies the country from the number itself or a provided hint

Get your free API key at isvalid.dev. The free tier includes 100 calls per day — enough for development and low-volume production use.

Full parameter reference and response schema: Phone Validation API docs →


5. Node.js code example

Using the IsValid SDK or the native fetch API.

import { createClient } from '@isvalid-dev/sdk';

const iv = createClient({ apiKey: process.env.ISVALID_API_KEY });

// ── Validate with full international number ─────────────────────────────────

const result = await iv.phone('+48600123456');

if (!result.valid) {
  console.log('Invalid phone number');
} else {
  console.log(`Valid ${result.type} number from ${result.countryCode}`);
  console.log('E.164:', result.e164);
  console.log('National:', result.national);
  console.log('International:', result.international);
  console.log('Calling code:', result.callingCode);
}

// ── Validate with national format + country hint ────────────────────────────

const result2 = await iv.phone('600123456', { countryCode: 'PL' });

if (result2.valid) {
  console.log(`Normalized: ${result2.e164}`);
  // → Normalized: +48600123456
}

In a registration or 2FA flow, you might use it like this:

// routes/auth.js (Express)
app.post('/register', async (req, res) => {
  const { phone, countryCode, ...userData } = req.body;

  let phoneCheck;
  try {
    phoneCheck = await validatePhone(phone, { countryCode });
  } catch {
    return res.status(502).json({ error: 'Phone validation service unavailable' });
  }

  if (!phoneCheck.valid) {
    return res.status(400).json({ error: 'Invalid phone number' });
  }

  // Reject non-mobile numbers for SMS-based 2FA
  if (phoneCheck.type !== 'MOBILE' && phoneCheck.type !== 'FIXED_LINE_OR_MOBILE') {
    return res.status(400).json({
      error: 'Please provide a mobile number for SMS verification',
    });
  }

  // Store the E.164-normalized number
  const user = await createUser({
    ...userData,
    phone: phoneCheck.e164,         // +48600123456
    phoneCountry: phoneCheck.countryCode, // PL
  });

  // Send OTP via Twilio / AWS SNS using the E.164 number
  await sendOtp(phoneCheck.e164);

  res.json({ success: true, userId: user.id });
});
Always store phone numbers in E.164 format (the e164 field). This is the format expected by Twilio, AWS SNS, Firebase Auth, and every major SMS/voice API. Use the national and international fields for display only.

6. cURL example

Validate an international phone number:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/phone?value=%2B48600123456"

With a country code hint (useful for national-format numbers):

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/phone?value=600123456&countryCode=PL"

Validate a US number:

curl -H "Authorization: Bearer YOUR_API_KEY" \
  "https://api.isvalid.dev/v0/phone?value=%2B12125551234"

7. Understanding the response

Valid Polish mobile number:

{
  "valid": true,
  "countryCode": "PL",
  "callingCode": "48",
  "nationalNumber": "600123456",
  "type": "MOBILE",
  "e164": "+48600123456",
  "national": "600 123 456",
  "international": "+48 600 123 456"
}

Invalid phone number:

{
  "valid": false
}
FieldTypeDescription
validbooleanWhether the phone number is valid according to the country's numbering plan
countryCodestringISO 3166-1 alpha-2 country code (e.g. "PL", "US", "GB")
callingCodestringThe country's international calling code without the + prefix (e.g. "48", "1", "44")
nationalNumberstringThe subscriber number without country code or national prefix
typestringLine type: MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE, TOLL_FREE, PREMIUM_RATE, VOIP, SHARED_COST, PERSONAL_NUMBER, PAGER, UAN, VOICEMAIL, or UNKNOWN
e164stringThe number in E.164 format — use this for storage and SMS APIs
nationalstringHuman-readable national format with grouping (e.g. "600 123 456")
internationalstringHuman-readable international format with country code (e.g. "+48 600 123 456")
ℹ️The type field is based on the country's numbering plan allocation. In some countries (like the US), mobile and fixed-line number ranges overlap — the API returns FIXED_LINE_OR_MOBILE in those cases.

8. Edge cases to handle

National format without a country hint

When a user enters 600 123 456 without a country code, the API cannot determine the country on its own — the same digit sequence could be valid in multiple countries. Always provide the countryCode parameter when accepting national-format input. You can infer the country from the user's IP address, browser locale, or a country selector in your form.

// Detect country from Accept-Language header as a fallback
function getCountryHint(req) {
  // Prefer explicit user selection
  if (req.body.countryCode) return req.body.countryCode;

  // Fall back to Accept-Language
  const lang = req.headers['accept-language']?.split(',')[0]; // e.g. "pl-PL"
  const country = lang?.split('-')[1]?.toUpperCase();
  return country || undefined;
}

VOIP numbers

VOIP numbers (Google Voice, Skype, etc.) are technically valid phone numbers, but they may not support SMS in all countries. If you need to send SMS for verification, check the type field and decide whether to accept VOIP numbers based on your use case. Some services allow VOIP numbers for voice-based OTP (a phone call with a code) but not for SMS-based OTP.

Toll-free numbers

Toll-free numbers (e.g. 1-800 in the US, 0800 in the UK) are valid numbers used by businesses for inbound calls. They cannot receive SMS and should not be accepted for 2FA or user registration. The API returns TOLL_FREE for these numbers so you can filter them appropriately.

Numbers with formatting characters

Users enter phone numbers with all kinds of formatting — spaces, dashes, dots, parentheses, and even the international dialing prefix 00 instead of +. The API handles all of these automatically. Pass the raw user input without pre-processing.

// All of these represent the same number — the API handles them all
'+48 600 123 456'     // international with spaces
'0048600123456'       // international dialing prefix (00)
'600-123-456'         // national with dashes (needs countryCode: 'PL')
'(600) 123 456'       // national with parentheses
'+48.600.123.456'     // dots as separators

Summary

Do not validate phone numbers with regex — formats differ across 200+ countries
Do not assume all phone numbers are 10 digits — lengths range from 7 to 15
Do not send SMS to FIXED_LINE or TOLL_FREE numbers — they cannot receive texts
Do not store phone numbers in local format — use E.164 for interoperability
Validate format, length, and line type in a single API call
Store the E.164 form for Twilio, AWS SNS, and other SMS gateways
Provide a countryCode hint when accepting national-format input
Check the type field to ensure MOBILE before sending SMS-based 2FA

See also

Validate phone numbers instantly

Free tier includes 100 API calls per day. No credit card required. Supports 200+ countries with E.164 normalization.