Phone Number Parsing Guide: E.164, libphonenumber, and Validation
Phone number validation looks simple until you accept users from more than one country. A field that passes ^\+?[0-9\s\-\(\)]+$ will happily accept +99 000 000 0000 (a country code that does not exist) and reject 020 7946 0958 (a perfectly valid UK landline entered locally). Use the Phone Number Parser to follow along with the examples in this guide.
E.164 — The Storage Standard
ITU-T E.164 is the international numbering plan that defines a canonical format for public telephone numbers. The rules are straightforward:
- Always starts with
+ - Followed by the country calling code (1–3 digits)
- Followed by the subscriber number (no spaces, dashes, or parentheses)
- Total maximum length: 15 digits (excluding the
+)
+[country code][subscriber number]
+14155552671 US number (country code 1)
+442079460958 UK number (country code 44)
+819012345678 Japan number (country code 81)
+97250000000 Israel number (country code 972)E.164 is machine-readable and globally unambiguous. It is the format databases, SMS APIs, VoIP systems, and carrier networks expect. Every phone number you store should be in E.164 format.
Why Regex Validation Fails Internationally
The fundamental problem is that phone number rules are not regular. They require a lookup table of country-specific metadata. Three concrete reasons:
Country calling codes are 1–3 digits. The North American Numbering Plan uses country code 1. The UK uses 44. Israel uses 972. A regex that assumes a fixed-length prefix will reject valid numbers or accept invalid ones.
Subscriber number length varies by country. A US number after the country code is always 10 digits. A UK number is 9 or 10 digits. A short-form French mobile can be 9 digits. No single length rule covers all cases.
Trunk prefixes are stripped in international format. When a UK user types their number locally they write 020 7946 0958 — the leading 0 is a trunk prefix used only for domestic dialing. In E.164 that same number becomes +442079460958 — the trunk 0 disappears. A regex cannot know which leading digits to strip without country-specific rules.
Beyond those three, number ranges are allocated in non-contiguous blocks: some prefixes are mobile, others landline, others premium-rate. Only a metadata database tracks this correctly.
libphonenumber — Google's Canonical Parser
Google's libphonenumber is the reference implementation for phone number parsing and validation. It ships a database of every country's numbering plan — valid number ranges, length rules, trunk prefixes, number types — and a parser that uses it.
The library has been ported to every major language. You almost certainly have access to it:
| Language | Package |
|---|---|
| JavaScript / TypeScript | libphonenumber-js |
| Python | phonenumbers |
| Ruby | phonelib |
| PHP | giggsey/libphonenumber-for-php |
| Go | nyaruka/phonenumbers |
| Java / Android | com.googlecode.libphonenumber (original) |
Parse / Format / Validate Cycle
The JS port libphonenumber-js exposes three core operations. Install it:
npm install libphonenumber-jsParse a user-supplied string into a structured phone number object, providing a defaultCountry hint for numbers entered without a country code:
import { parsePhoneNumber } from 'libphonenumber-js';
const phone = parsePhoneNumber('020 7946 0958', 'GB');
// phone.country => 'GB'
// phone.number => '+442079460958' (E.164)
// phone.isValid() => trueValidate with isValid(). This checks that the number falls within an allocated range for its country — not just that the digit count is correct:
import { parsePhoneNumber, isValidPhoneNumber } from 'libphonenumber-js';
isValidPhoneNumber('+14155552671') // true — valid US number
isValidPhoneNumber('+1415555000') // false — too short
isValidPhoneNumber('+99123456789') // false — country code 99 does not existMetadata tradeoff. The full metadata bundle is ~145 KB gzipped. For client-side use there are lighter variants:
parsePhoneNumberfrom'libphonenumber-js'— full metadata, most accurateparsePhoneNumberfrom'libphonenumber-js/min'— ~50 KB, skips some extended validationparsePhoneNumberfrom'libphonenumber-js/mobile'— mobile numbers onlyparsePhoneNumberfrom'libphonenumber-js/max'— includes carrier/geocoding data
Display Formatting
Once you have a parsed phone number object, libphonenumber-js can format it for four different display contexts:
import { parsePhoneNumber } from 'libphonenumber-js';
const phone = parsePhoneNumber('+442079460958');
phone.formatInternational() // '+44 20 7946 0958'
phone.formatNational() // '020 7946 0958'
phone.format('E.164') // '+442079460958'
phone.getURI() // 'tel:+442079460958'| Format | Example | Use When |
|---|---|---|
| INTERNATIONAL | +44 20 7946 0958 | Displaying to users, UI labels |
| NATIONAL | 020 7946 0958 | Displaying to users in the same country |
| E.164 | +442079460958 | Database storage, API calls, SMS sending |
| RFC3966 | tel:+44-20-7946-0958 | href="tel:..." links, SIP headers |
For href="tel:..." links, use getURI() which returns the RFC3966 form. Most mobile browsers and VoIP clients understand this format.
Storage Recommendation
Always store phone numbers in E.164. The reasons are practical: E.164 is globally unique, indexable, and passes straight to SMS and telephony APIs without transformation. National format storage creates ambiguity — 0207 946 0958 is meaningless without knowing the country.
Three rules for a solid storage strategy:
- Store E.164 in the database. A
VARCHAR(16)column is sufficient (15 digits + the+sign). - Display locally at render time. Pass the stored E.164 number through
formatNational()orformatInternational()depending on the viewer's locale. - Preserve the country hint. Store a separate
countrycolumn (ISO 3166-1 alpha-2, e.g.GB) alongside the phone number. This lets you format nationally without re-parsing.
Handling Extensions
Business phone numbers often have extensions. The standard ways to represent them are:
+14085551212;ext=5678 (RFC 3966 — preferred)
+1 (408) 555-1212 ext. 5678 (human-readable)
+14085551212x5678 (informal shorthand)libphonenumber-js parses extensions automatically when they appear in common formats. Access the extension via the ext property:
import { parsePhoneNumber } from 'libphonenumber-js';
const phone = parsePhoneNumber('+14085551212 ext. 5678', 'US');
phone.number // '+14085551212'
phone.ext // '5678'
phone.getURI() // 'tel:+14085551212;ext=5678'When building tel: URIs with extensions, the ;ext= parameter is the correct RFC 3966 form. Most softphones and mobile dialers handle it correctly.
Country Detection
To parse a number entered without a country code (national format), you need a defaultCountry hint. There are several ways to obtain it:
- IP geolocation — fast and automatic, accurate enough for a default. Use a server-side lookup on first request.
Accept-Languageheader — a rough signal.en-GBsuggests UK but does not guarantee it.- User-provided country — a country selector next to the phone input. Highest accuracy. Required for financial and identity verification flows.
- Existing profile data — if the user has a billing address, use its country code.
import { parsePhoneNumber } from 'libphonenumber-js';
// User in France entering a national number
const phone = parsePhoneNumber('06 12 34 56 78', 'FR');
phone.number // '+33612345678'
phone.isValid() // truePitfalls
SMS short codes follow completely different rules. Short codes (e.g. 12345 in the US) are 5–6 digits and are never valid E.164 numbers. isValidPhoneNumber will correctly return false for them. If your application sends to short codes, validate them separately with your SMS provider's rules.
VoIP numbers pass validation but may not be reachable by SMS. libphonenumber validates that a number is in an allocated range, but VoIP numbers (like Google Voice, Twilio provisioned numbers) are valid E.164 numbers. SMS deliverability is a carrier-level concern that requires a number lookup API (e.g. Twilio Lookup, Vonage Number Insight) to determine.
Mobile vs landline detection is not reliable in all regions. The getType() method returns MOBILE, FIXED_LINE, or FIXED_LINE_OR_MOBILE (when the type cannot be determined from metadata alone). In many countries — including the US — all numbers in a given area code can be ported to mobile, so type detection is unreliable.
Possible vs valid. isPossiblePhoneNumber() only checks digit length — fast but imprecise. isValidPhoneNumber() checks the full range allocation — slower but correct. Use isValidPhoneNumber for user input validation.
Parse and validate phone numbers directly in your browser with the Phone Number Parser — supports all E.164 countries, extension parsing, and all four display formats, with no data leaving your machine. For related guides see IBAN Validation Guide and Testers Guide.