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.
In this guide
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.
| Country | Code | Example (E.164) | National format | Digits |
|---|---|---|---|---|
| United States | +1 | +12125551234 | (212) 555-1234 | 10 |
| United Kingdom | +44 | +447911123456 | 07911 123456 | 10-11 |
| Germany | +49 | +491701234567 | 0170 1234567 | 10-11 |
| Poland | +48 | +48600123456 | 600 123 456 | 9 |
| Japan | +81 | +819012345678 | 090-1234-5678 | 10 |
| Brazil | +55 | +5511912345678 | (11) 91234-5678 | 10-11 |
| India | +91 | +919876543210 | 09876 543210 | 10 |
| Australia | +61 | +61412345678 | 0412 345 678 | 9 |
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:
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.
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 }); });
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 }
| Field | Type | Description |
|---|---|---|
| valid | boolean | Whether the phone number is valid according to the country's numbering plan |
| countryCode | string | ISO 3166-1 alpha-2 country code (e.g. "PL", "US", "GB") |
| callingCode | string | The country's international calling code without the + prefix (e.g. "48", "1", "44") |
| nationalNumber | string | The subscriber number without country code or national prefix |
| type | string | Line type: MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE, TOLL_FREE, PREMIUM_RATE, VOIP, SHARED_COST, PERSONAL_NUMBER, PAGER, UAN, VOICEMAIL, or UNKNOWN |
| e164 | string | The number in E.164 format — use this for storage and SMS APIs |
| national | string | Human-readable national format with grouping (e.g. "600 123 456") |
| international | string | Human-readable international format with country code (e.g. "+48 600 123 456") |
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
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.