Developers

E.164 phone number format: a practical guide for developers

How to normalize phone numbers to E.164 in JavaScript and Python, the pitfalls that break deduplication, and how the MobileValidate API normalizes input.

By Published 8 min read

On this page

E.164 is the canonical way to write a phone number: +, the country code, then the national number, with no spaces and at most 15 digits. Converting every input to E.164 before you store, deduplicate or send is the single cheapest data-quality fix in a phone pipeline. This guide shows how to do it in JavaScript and Python, and which inputs trip people up.

What exactly is an E.164 number?

E.164 is the international numbering plan published by the ITU (ITU-T E.164, 02/2026 edition). A number in this format has three parts: a + that stands for the international dialling prefix, a country code of one to three digits, and the national significant number without any trunk prefix. The whole thing is at most 15 digits.

Input as a person typed itE.164
07911 123456 (typed in the UK)+447911123456
+44 (0)20 7123 4567+442071234567
0049 151 23456789+4915123456789
(415) 555-2671 (typed in the US)+14155552671
8 (912) 345-67-89 (typed in Russia)+79123456789

All five come from real parsing runs with libphonenumber-js 1.13 (September 2026). The rows show three different trunk prefixes being removed: the UK 0, the German 0 after 0049, and the Russian 8. The E.164 glossary entry covers the basics. The rest of this guide is about what goes wrong in production.

A terminal sends an API request and a JSON response returns true, false and null values.A terminal sends an API request and a JSON response returns true, false and null values.
One REST call. Every answer is true, false or null (unknown).

Why not just strip non-digits and add a plus?

Because the digits alone don't tell you the country, and some digits must be removed, not kept. A naive replace(/\D/g, "") gets these cases wrong:

  • Trunk prefixes. 07911 123456 becomes 07911123456. Adding +44 in front gives +4407911123456, which is not a number. The leading 0 has to go.
  • International access codes. 0049 151… and 00 91 98765 43210 start with 00, which means "international" in most of the world. In North America the access code is 011.
  • The "(0)" convention. +44 (0)20 7123 4567 is common on UK and German business cards. The (0) tells domestic callers to dial a 0. It is not part of the E.164 number.
  • National format with no country. 4155552671 could be a US number, or it could be read as +41 55 552 671, a Swiss-looking number. Only context decides.

A numbering-plan library solves the first three and makes you state the fourth. Google's libphonenumber is the reference implementation, and ports exist for most languages. It carries metadata about each country's ranges and number lengths, so it can reject numbers that can't exist, not just format them.

How do you normalize a number in JavaScript?

In Node.js or the browser, use libphonenumber-js. Import the /max metadata if you want real validation and line-type hints. The smaller default metadata only checks lengths.

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

export function toE164(input, defaultCountry) {
  const raw = String(input ?? "").trim();
  if (!raw || raw.length > 32) return { ok: false, reason: "unparseable" };
  const p = parsePhoneNumberFromString(raw, defaultCountry); // defaultCountry: "GB", "US", …
  if (!p) return { ok: false, reason: "unparseable" };
  if (!p.isValid()) return { ok: false, reason: "invalid" };
  return { ok: true, e164: p.number, country: p.country ?? null, type: p.getType() ?? null };
}

toE164("07911 123456", "GB");      // { ok: true, e164: "+447911123456", country: "GG", type: "MOBILE" }
toE164("+44 (0)20 7123 4567");     // { ok: true, e164: "+442071234567", country: "GB", type: "FIXED_LINE" }
toE164("+1 (415) 555-2671");       // { ok: true, e164: "+14155552671", country: "US", type: "FIXED_LINE_OR_MOBILE" }
toE164("415-555-2671", "GB");      // { ok: false, reason: "invalid" }

Two details matter. isValid() checks the number against the country's ranges, while isPossible() only checks the length. Use isValid() before paying for anything downstream. Also, p.number is already the E.164 string. Don't rebuild it yourself from countryCallingCode and nationalNumber.

How do you normalize a number in Python?

The phonenumbers package is the Python port of libphonenumber. Its core API is parse, is_valid_number, format_number and number_type:

Python
import phonenumbers
from phonenumbers import NumberParseException, PhoneNumberFormat, PhoneNumberType

def to_e164(raw: str, default_region: str | None = None):
    try:
        p = phonenumbers.parse(raw, default_region)   # region like "GB"; None requires a leading +
    except NumberParseException:
        return None, "unparseable"
    if not phonenumbers.is_valid_number(p):
        return None, "invalid"
    e164 = phonenumbers.format_number(p, PhoneNumberFormat.E164)
    return e164, phonenumbers.number_type(p)          # e.g. PhoneNumberType.MOBILE

to_e164("07911 123456", "GB")     # ('+447911123456', PhoneNumberType.MOBILE)
to_e164("4155552671")             # raises inside parse -> (None, 'unparseable'): no region, no +

parse raises NumberParseException when it can't interpret the input at all, for example national digits with no region. Catch it and treat it like an invalid number. number_type returns an enum such as MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE, VOIP or TOLL_FREE. That value comes from the numbering plan, so it describes the range, not the current service. Pin the library version and upgrade it regularly: numbering plans change, and the metadata ships with the package.

Which inputs cause the most surprises?

These are from our own parsing runs, not from theory:

InputWhat happensWhy it matters
+44 7911 123456Country comes back as GG (Guernsey), not GB+44 is shared by the UK, Guernsey, Jersey and the Isle of Man. Don't map country code to country with a lookup table
+1 800 555 0199Valid, type TOLL_FREEValid is not the same as "can receive SMS"
+1 415 555 2671Type FIXED_LINE_OR_MOBILEUS and Canadian ranges don't separate mobile from fixed. You need a carrier lookup to know
+33 06 12 34 56 78Parsed as +33612345678The library drops a trunk 0 written after the country code
+44 7911 123456 ext 12E.164 +447911123456, extension 12 kept separatelyE.164 has no extensions. Store the extension in its own field or you lose it
+447911123456 (full-width digits)Parsed correctlyCopy-paste from some keyboards and PDFs produces non-ASCII digits. Don't reject them with a strict regex before parsing
447911123456 with no regionNot parsed by the libraryWhether bare digits mean "international" is your decision, not the library's

The Guernsey case is the one that silently breaks analytics. If your dashboard groups by country and expects every +44 number to be GB, a slice of your UK traffic disappears into a country you never selected.

How does the MobileValidate API normalize numbers?

The API converts every number to E.164 before deduplication, caching, pricing or any check, using libphonenumber metadata. You can send any format. The rules:

  1. Input is trimmed. Empty strings and inputs over 32 characters are rejected.
  2. If the input is 8 to 15 bare digits and the request has no default_country, a + is added, so the digits are read as an international number with country code.
  3. Otherwise the input is parsed with default_country (ISO 3166-1 alpha-2, one value per request).
  4. The number must be valid for its numbering plan, not just the right length. Otherwise number_status is invalid_number.
  5. After conversion, repeats of the same E.164 number in one request are marked duplicate.

Invalid and duplicate rows are never checked. You're not charged for inconclusive results (unknown, unsupported country, timeout, invalid, duplicate). Here is a real test-mode request with mixed formats and default_country: "GB":

Shell
curl https://api.mobilevalidate.com/v1/lookup \
  -H "Authorization: Bearer $MOBILEVALIDATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"numbers": ["+44 (0)20 7123 4567", "07911 123456", "447911123456", "0049 151 23456789",
                   "415-555-2671", "+44 7911 123456 ext 12"],
       "checks": ["carrier"], "default_country": "GB", "wait": 5}'

The number fields of each result (excerpt):

JSON
[
  {"input": "+44 (0)20 7123 4567",    "e164": "+442071234567",  "country": "GB", "number_status": "valid"},
  {"input": "07911 123456",           "e164": "+447911123456",  "country": "GG", "number_status": "valid"},
  {"input": "447911123456",           "e164": "+447911123456",  "country": "GG", "number_status": "duplicate"},
  {"input": "0049 151 23456789",      "e164": "+4915123456789", "country": "DE", "number_status": "valid"},
  {"input": "415-555-2671",           "e164": null,             "country": null, "number_status": "invalid_number"},
  {"input": "+44 7911 123456 ext 12", "e164": "+447911123456",  "country": "GG", "number_status": "duplicate"}
]

Three different inputs collapse into one number, so only one of them is checked and billed. The US number in national format is invalid because the request's default country is GB.

What goes wrong with default_country?

default_country applies to the whole request, so a mixed-country list in national format will lose rows. Our test runs show two traps:

  • Wrong default. 415-555-2671 in a request with default_country: "GB" is invalid_number. The same number with "US", or written as +1 415 555 2671, is valid.
  • No default. 4155552671 with no default_country gets a + and becomes +4155552671. That is read as country code 41, fails validation and comes back invalid_number.

The fix is to keep the country with the number from the start. Capture it on the form (a country picker next to the phone field), store it with the record, and when you batch, group numbers by country and send one request per group. If your source data is already international, + plus country code, leave default_country out. For bulk lists, POST /v1/jobs/estimate shows the counts of valid, invalid and duplicate rows for free, so you can catch a wrong default before running a job. See how to clean a phone number list in bulk.

How should you store and compare numbers?

A schema that avoids most future bugs:

ColumnExampleNotes
phone_e164+447911123456The canonical value. Index it, deduplicate on it, join on it
phone_raw07911 123456What the person typed, for support. Don't use it for logic
phone_countryGGFrom the parser, not from the country code
phone_ext12Only if you accept extensions
phone_checked_at2026-09-25T16:06:07ZWhen any lookup last ran, so you know how old the data is

Store E.164 as a string, never an integer. Compare with exact string equality after normalization. Mask numbers in logs (for example +44791*****56), because a full phone number is personal data. For tests and documentation, use ranges that are never assigned: Ofcom reserves 07700 900000 to 07700 900999 for drama (Ofcom), and our test mode uses numbers from that range.

What doesn't E.164 tell you?

A valid E.164 number is a number that could exist. It isn't proof that the number is assigned, switched on, a mobile, or used by the person who typed it. Once the format is right, the next questions are:

HLR vs MNP vs number validation compares these layers side by side.

What are the key takeaways?

  • Normalize to E.164 with a numbering-plan library (libphonenumber or a port), not with regexes.
  • Use full validation (isValid() / is_valid_number) rather than length checks before you pay for any downstream check.
  • Keep the country with the number. default_country applies to a whole request, so group mixed lists by country.
  • Don't infer country from country code: +44 can come back as GG, and +1 covers many countries.
  • Deduplicate after normalization. The API does this for you, and duplicates are never charged.
  • Store E.164 as a string, keep the raw input and the extension separately, and mask numbers in logs.

Sources

  1. ITU-T Recommendation E.164: The international public telecommunication numbering plan — International Telecommunication Union, 2026
  2. libphonenumber — Google, 2026
  3. Telephone numbers for use in TV and radio drama programmes — Ofcom, 2026

Frequently asked questions

What is the E.164 format?

A plus sign, the country code and the national number, with no spaces or punctuation and at most 15 digits. +447911123456 is a UK-format mobile number written in E.164. The format comes from ITU-T Recommendation E.164.

Should I store phone numbers as integers?

No. Store the E.164 string. Integers lose the leading plus, can overflow in some languages, and invite arithmetic nobody should do on a phone number. Keep the raw input in a separate field if you need it for support.

Why does a US number fail when my default country is GB?

A number without a leading + or 00 is read in the default country's numbering plan. 415-555-2671 is a valid US number but not a valid UK number, so it is rejected. Send international format, or group numbers by country and send one default_country per request.

Does a valid E.164 number mean the phone works?

No. Validation checks the format and the numbering plan. It says nothing about whether the number is assigned, switched on or used on a messaging app. Use a carrier lookup or channel checks for that.

All articles

Know before you send.

Tell us about your use case. We review every request and set you up with test and live keys.