# 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.

Canonical: https://mobilevalidate.com/blog/e164-phone-number-format-guide-for-developers · Last updated: 2026-09-25

![Cover: E.164 phone number format: a practical guide for developers](https://mobilevalidate.com/og/blog/e164-phone-number-format-guide-for-developers.png)


By MobileValidate team (https://mobilevalidate.com/about) · Published: 2026-09-25 · Category: Developers · Tags: E.164, Phone number formatting, Libphonenumber, Data quality, API

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](https://www.itu.int/rec/T-REC-E.164/en)). 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 it | E.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](/glossary/e164) covers the basics. The rest of this guide is about what goes wrong in production.

## 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](https://github.com/google/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.

```js
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:

| Input | What happens | Why it matters |
|---|---|---|
| `+44 7911 123456` | Country 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 0199` | Valid, type `TOLL_FREE` | Valid is not the same as "can receive SMS" |
| `+1 415 555 2671` | Type `FIXED_LINE_OR_MOBILE` | US and Canadian ranges don't separate mobile from fixed. You need a [carrier lookup](/services/carrier-lookup) to know |
| `+33 06 12 34 56 78` | Parsed as `+33612345678` | The library drops a trunk `0` written after the country code |
| `+44 7911 123456 ext 12` | E.164 `+447911123456`, extension `12` kept separately | E.164 has no extensions. Store the extension in its own field or you lose it |
| `+447911１２３４５６` (full-width digits) | Parsed correctly | Copy-paste from some keyboards and PDFs produces non-ASCII digits. Don't reject them with a strict regex before parsing |
| `447911123456` with no region | Not parsed by the library | Whether 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"`:

```bash
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](/blog/how-to-clean-a-phone-number-list-in-bulk).

## How should you store and compare numbers?

A schema that avoids most future bugs:

| Column | Example | Notes |
|---|---|---|
| `phone_e164` | `+447911123456` | The canonical value. Index it, deduplicate on it, join on it |
| `phone_raw` | `07911 123456` | What the person typed, for support. Don't use it for logic |
| `phone_country` | `GG` | From the parser, not from the country code |
| `phone_ext` | `12` | Only if you accept extensions |
| `phone_checked_at` | `2026-09-25T16:06:07Z` | When 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](https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama)), and our [test mode](/docs/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:

- **What kind of line is it?** The library's type is range-based. A [carrier lookup](/services/carrier-lookup) returns the current `line_type` and carrier, which matters in the US and Canada and for [ported numbers](/blog/mobile-number-portability-why-carrier-lookups-can-be-wrong).
- **Is it live?** That needs a network query. Our [HLR lookup](/services/hlr-lookup) is coming soon. See [how to check if a phone number is active](/blog/how-to-check-if-a-phone-number-is-active).
- **Does it use a given channel?** Channel checks such as the [WhatsApp check](/services/whatsapp-number-check) answer that for opted-in contacts.

[HLR vs MNP vs number validation](/blog/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](https://www.itu.int/rec/T-REC-E.164/en) — International Telecommunication Union, 2026
2. [libphonenumber](https://github.com/google/libphonenumber) — Google, 2026
3. [Telephone numbers for use in TV and radio drama programmes](https://www.ofcom.org.uk/phones-and-broadband/phone-numbers/numbers-for-drama) — 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.
