# How to store phone numbers in E.164

Store one E.164 string, the extension and the raw input. How trunk prefixes, 00 and 011, extensions and did_you_mean work, and why a regex is not enough.

Date: 2026-09-27

Store a phone number as one E.164 string, such as `+442079460018`: a plus sign, the country calling code and the national number, with no spaces, no trunk prefix and no extension. Keep it in a text column, put any extension in a column of its own, and keep what the person typed beside both. The national and international spellings and the `tel:` link are derived from those values when a number is displayed.

Lists go wrong on the way to that string: a UK number written with a bracketed 0, a US export with 011 left in front, a CRM column that lost its plus signs, an office number ending in "x204". Each case below shows the answer Spaw's phone lookup gives, run on 27 September 2026 against libphonenumber 9.0.37 and 9.0.40, with the `country` that was sent; the last section explains why a regular expression cannot do the job. The [phone number formatter](/tools/phone-number-formatter) reads input the same way, so any example can be pasted there to see its E.164 form, extension and `did_you_mean`. It checks the numbering plan only and reads no regulator's table, so the `+442030130018` row of the last table shows there as fitting the plan.

## What to store

| Column | Example | Why |
| --- | --- | --- |
| `e164` | `+12025550143` | The key: deduplicate, join and send by it. A number has exactly one E.164 form. |
| `extension` | `204` | E.164 has no place for an extension, and the number is dialled without it. |
| `phone_raw` | `+1 202 555 0143 x204` | What was typed, so the number can be read again when the numbering metadata changes. |
| `phone_country` | `US` | The country the input was read against, when it was written without a calling code. |

+1 202 555 0143 sits in the North American 555-0100 to 555-0199 range, which NANPA keeps reserved for entertainment and advertising (https://www.nanpa.com/numbering/555-line-numbers, accessed 27 September 2026), so the lookup answers it valid with `is_fictional: true`; it stands in for an office number throughout this guide.

Use a text column, never an integer. libphonenumber's list of falsehoods programmers believe about phone numbers (https://github.com/google/libphonenumber/blob/master/FALSEHOODS.md, accessed 27 September 2026) puts it plainly: 007, 07 and 7 are the same number but not necessarily the same phone number, because in some countries a leading 0 is part of the number. The same page notes that ITU-T limits a phone number to fifteen digits and that Germany has assigned longer ones. libphonenumber's metadata allows German fixed-line numbers of up to 17 digits with the calling code, and Indonesian toll-free numbers of up to 19. Spaw's own suppression and feedback tables hold E.164 values in 20-character columns.

Do not store `valid` or the line type as permanent facts about a row. The falsehoods page advises not storing them at all and checking again when they are needed: a number that connects today may be disconnected tomorrow, a number of one type can be reassigned to another, and a plan can insert a digit into existing numbers. If you keep an answer, keep the date it was given, and look the number up again when a decision depends on it.

## A national number needs a country

`07400 123456` sent with no country answers `valid: false` with the reason [`missing_country`](/docs/reasons/missing_country), free: without a calling code, a national number could belong to any plan. Sent with `country: "GB"`, the same input answers `+447400123456`, and so do `7400123456` and `447400123456` sent with GB, because libphonenumber recognises a leading calling code that matches the country sent. Sent with US, `2025550143`, `1 202 555 0143` and `12025550143` all answer `+12025550143`.

The country is a hint, and the wrong hint can produce a valid answer for the wrong country. `447400123456` sent with DE answers valid as a German landline number, because those twelve digits fit Germany's plan; nothing in the digits shows that they were a UK mobile number that lost its plus. When a form collects a country, send it. When a CSV has no country column, send the country the list came from, and prefer values that start with a plus. An API key can carry a default country, applied only when a request names none.

## Trunk prefixes: dialled at home, dropped after the calling code

A trunk prefix is what is dialled before a number inside its own country and left out after the calling code: the 0 of `020` in London, the 1 in front of a North American long-distance number. libphonenumber's metadata (the same in 9.0.37 and 9.0.40) describes 245 numbering regions: 113 use 0 as their trunk prefix, 101 have none, and 31 use another one, which is 1 in the North American regions and the Marshall Islands, 8 in Russia, Kazakhstan, Belarus and Turkmenistan, and 06 in Hungary. The [formatter](/tools/phone-number-formatter) lists every region whose trunk prefix is something other than 0, and the 17 without one whose numbers keep a leading 0; each region's page under [phone facts by country](/tools/phone-number-lookup/countries) states its trunk and international prefixes.

Getting the trunk prefix wrong produces two mistakes.

**Keeping the trunk 0.** `+44 (0)7400 123456` is a common way to write a UK number for readers at home and abroad at once. ITU-T Recommendation E.123 (02/2001), the standard for writing numbers down (https://www.itu.int/rec/T-REC-E.123, accessed 27 September 2026), uses parentheses for digits that are not always dialled and says they should not be used in an international number (clause 7.2). The lookup answers it valid, `+447400123456`, and does the same for `+4407400123456`, where the 0 was kept without brackets. A database is less forgiving: `+4407400123456` and `+447400123456` are different strings, so a table keyed on what was typed holds the same person twice.

**Dropping a 0 that belongs to the number.** In 17 of the regions with no trunk prefix, a number can itself start with 0, and the 0 stays after the calling code. Italy is the usual example: the falsehoods page records that since 1998 the old prefix has been part of the number. libphonenumber's example Italian landline `02 1234 5678` is `+39 02 1234 5678`, E.164 `+390212345678`. `+39 2 1234 5678`, with the 0 removed as if it were a UK trunk prefix, answers `invalid_number`. The [Italy](/tools/phone-number-lookup/countries/it) and [United Kingdom](/tools/phone-number-lookup/countries/gb) pages show both plans.

North America is the third case. Its trunk prefix is 1, the same digit as its calling code, so `1 202 555 0143` sent with US and `+1 202 555 0143` are the same number, and the [United States](/tools/phone-number-lookup/countries/us) page describes the plan behind the 1.

## International prefixes: 00, 011 and the rest

The plus sign stands for whatever is dialled to leave the caller's country, which differs from country to country (E.123, clause 7.1). In the same metadata, 00 is the international prefix libphonenumber gives for 185 of the 245 regions, and the patterns of seven more, Ukraine's among them, also accept it. Twenty-six dial only 011: the 25 North American regions and the Marshall Islands. Palau's pattern accepts 011 or 012, and Singapore's any three digits from 000 to 039, so both accept 011 too. The rest dial something else, such as 0011 in Australia, 010 in Japan, or 8~10 (dial 8, wait for a tone, dial 10) in Russia, Kazakhstan, Belarus, Tajikistan and Turkmenistan. People type the prefix they dial, so lists carry all of these.

Spaw reads the two common prefixes differently, on purpose.

| Input | Country sent | Answer |
| --- | --- | --- |
| `00447400123456` | US | valid, `+447400123456` |
| `0044 7400 123456` | none | valid, `+447400123456` |
| `011 44 7400 123456` | US | valid, `+447400123456` |
| `011447400123456` | GB | invalid, `too_long`, `did_you_mean: "+447400123456"` |

A leading 00 is read as a plus sign whatever country is sent, whenever the country's own reading of the digits is not a valid number, and the result is kept only if it is valid. Most of the world dials 00. A number read this way is an ordinary valid answer, charged like any other. Where 011 is one of the country's own international prefixes, libphonenumber reads it directly, as with `011 44 7400 123456` sent with US. Anywhere else, 011 is never removed on the lookup's own authority. In the UK the area codes of Leeds (0113), Sheffield (0114), Leicester (0116), Bristol (0117) and Reading (0118) all begin with those three digits. The lookup answers such input invalid and offers the other reading as `did_you_mean` when it is the only valid one.

## did_you_mean is a suggestion, never a correction

`did_you_mean` appears on invalid answers only, and only when a country was sent. It carries the E.164 form of the one valid reading of what was typed: the digits with a plus added, when they land in another country, or the digits without a foreign international prefix that was left in front of them, such as 011 sent with a country that does not dial it.

```bash
curl https://spaw.co/api/v1/phone \
  -H "Authorization: Bearer sk_live_…" \
  -H "Content-Type: application/json" \
  -d '{"phone": "447400123456", "country": "US"}'
```

```json
{
  "success": true,
  "data": {
    "valid": false,
    "reason": "too_long",
    "e164": null,
    "did_you_mean": "+447400123456",
    "// 45 more fields, then sources": "see the phone intelligence guide"
  },
  "meta": { "credits_used": 0, "credits_remaining": 100, "cache_hit": false }
}
```

Those are four of the 49 fields under `data`. The answer stays invalid and free, and nothing is applied. The field is null when no reading is valid, when more than one is, when the reading lands in a block the regulator's table says nobody holds (unless it sits in a range reserved for fiction), and when the input could be a national number of the country sent with one digit too many or too few. `33612345678` sent with US shows the last case. With a plus, those digits are libphonenumber's example French mobile number, `+33 6 12 34 56 78`, but eleven digits is also what a US number with one extra keystroke looks like, so the answer is [`too_long`](/docs/reasons/too_long) with no suggestion.

Show the suggestion to the person who typed the number and let them confirm it. On an imported list, write it to a review column and never over the stored value: a corrected number can belong to someone who never gave it to you.

## Extensions stay out of E.164

`+1 202 555 0143 x204` answers `e164: "+12025550143"` and `extension: "204"`. The display forms keep the extension: `national` reads `(202) 555-0143 ext. 204`, and `rfc3966` reads `tel:+1-202-555-0143;ext=204`. The `;ext=` parameter is the one RFC 3966, the `tel` URI specification (December 2004, https://www.rfc-editor.org/rfc/rfc3966), defines. The spellings `ext. 204`, `extension 204`, `#204` and `;ext=204` all parse to the same two values. E.123 draws the same line on paper (clause 2.7): the extension is written after the number, so it stays apart from the digits to be dialled.

Because the extension is not part of E.164, two extensions of one switchboard share an `e164`. Spaw's bulk runs recognise repeats the same way: a row with `x205` after a row with `x204` on the same number is marked `is_duplicate`, answered free, and left out of the `deduped` download, although its own answer still carries `extension: "205"`. If a contacts table needs both, make its unique key `(e164, extension)` rather than `e164` alone, and store an empty string rather than NULL when there is no extension: PostgreSQL (unless the index is declared `NULLS NOT DISTINCT`), MySQL and SQLite let a unique index hold any number of rows whose extension is NULL.

## Why a regex is not enough

The widely copied pattern `^\+[1-9]\d{1,14}$` describes the shape of an E.164 string. It is a reasonable check on a column that is already normalised, widened if the column must admit the longer German and Indonesian numbers above. It cannot validate what people type: it rejects `+44 (0)7400 123456`, which is a valid number, and it accepts every row below.

| Input | Pattern | libphonenumber 9.0.37 and 9.0.40 | Spaw |
| --- | --- | --- | --- |
| `+39212345678` | accepts | invalid: Italy's leading 0 was dropped | `invalid_number` |
| `+12025550143` | accepts | valid | valid, `is_fictional: true` (555-01XX) |
| `+447700900123` | accepts | invalid: Ofcom's drama range is not in the metadata | valid, `is_fictional: true` |
| `+442030130018` | accepts | valid | invalid, `unassigned_block`: Ofcom lists 020 3013 as protected, with no holder |
| `+13575550143` | accepts | invalid: area code 357 is not in the metadata yet | `invalid_number` |

A pattern knows lengths. The numbering plan, which libphonenumber encodes, knows which ranges exist and what type each range is. The regulators' allocation tables know which blocks somebody holds: of the 343,860 number blocks in Ofcom's table as synced on 26 September 2026, 218,840 are free, protected, quarantined or reserved in Ofcom's terms, which Spaw reads as [unassigned](/guides/what-an-unassigned-block-means). And the plan cannot say whether a North American number is a mobile; [the carrier the block was originally allocated to sometimes can, until the number is ported](/guides/why-us-numbers-cannot-be-split-into-mobile-and-landline).

The last row is a limit Spaw shares. NANPA's central office code report of 26 September 2026 (https://www.nanpa.com/reports/co-code-reports) lists 33 assigned central office codes in four overlay area codes, 357 and 837 in California, 483 in Alabama and 861 in Illinois, all assigned between 7 August and 25 September 2026 and none yet marked in service. libphonenumber 9.0.37 and 9.0.40 do not include those area codes, and Spaw decides validity from the metadata, and its bundled list of fiction ranges, before it reads any regulator table, so a number in them answers `invalid_number` until a metadata update adds them. That is one more reason to keep the raw input: a number rejected today can be read again later.

## Checklist

- Parse on the server, with the country the input was written for, and store `e164`, `extension` and the raw input.
- Derive `national`, `international` and the `rfc3966` link when displaying a number, and never key on them.
- Make `e164` unique, or `(e164, extension)` for switchboard contacts, with an empty extension rather than NULL when there is none.
- Offer `did_you_mean` to the person who typed the number, and never apply it to a list.
- Treat validity and line type as dated answers, not permanent columns.
- Paste a column into the [formatter](/tools/phone-number-formatter) to see how each row reads, and see the [E.164 glossary entry](/glossary/e164) and the [phone intelligence guide](/docs/phone-intelligence) for every field.

Reference: https://spaw.co/guides/how-to-store-phone-numbers-in-e164
