# Phone Number Validator & Formatter – E.164, Country, Line Type (`far003/phone-number-validator-e164`) Actor

Validate and format phone numbers offline with Google libphonenumber metadata: E.164, international and national format, country, line type (mobile, fixed, toll-free, premium...). Clean CRM lists before an import or an SMS send. No live carrier lookup.

- **URL**: https://apify.com/far003/phone-number-validator-e164.md
- **Developed by:** [Francesco Antonio Russo](https://apify.com/far003) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.50 / 1,000 validated phone numbers

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Phone Number Validator & Formatter – E.164, country, line type

Clean a list of phone numbers **before** you import it into a CRM or send an SMS or WhatsApp campaign. Give it numbers written any way people write them – `(212) 555-0123 x55`, `0044 20 7946 0958`, `+39 06 6982` – and get back one tidy row per number: valid or not (with the reason), the **E.164**, international, national and RFC 3966 forms, the country, and the **line type** (mobile, fixed line, toll-free, premium rate, VOIP…).

Everything is computed **offline** from the numbering-plan metadata of Google's [libphonenumber](https://github.com/google/libphonenumber), packaged in `libphonenumber-js` 1.13.15. There is no call to any phone network, carrier or third-party service: the result is the same every time, it is fast, and it is cheap.

### Who it is for

- **CRM and spreadsheet cleanup.** Normalise a messy phone column to E.164 so that duplicates match and imports do not fail.
- **SMS / WhatsApp campaign preparation.** Drop numbers that cannot exist, keep mobiles, flag premium-rate and special numbers before you pay to send to them.
- **Form and lead data.** Check what people typed in a sign-up form, with the country they picked.
- **Data pipelines.** Run it on a dataset from another Actor (a scraper, a lead list) and get a clean dataset back.

### What it does NOT do (read this first)

- **No live lookup.** It does not ask any carrier or network. It cannot tell you whether a number is *connected*, *active*, *reachable*, *ported* or *disconnected* (that is what an HLR lookup does). "Valid" here means: *the number has the right length and matches a pattern of the numbering plan of its country*.
- **Line type is deduced from the numbering plan,** not looked up. In the USA, Canada and a few other countries, fixed-line and mobile numbers share the same ranges and cannot be told apart: you get `FIXED_LINE_OR_MOBILE`. A number that was ported to another type of line keeps the type of its original range.
- **No owner, no name, no carrier, no location beyond the country.**
- **It does not find numbers in free text.** Each entry must be one phone number (with an optional extension). A sentence with a number inside is reported as `CONTAINS_LETTERS`.

### What you get

For every number that gets a result, one row in the dataset. Every row has the same 22 fields, in the same order; a field that does not apply is `null`.

| Field | Type | What it holds |
|---|---|---|
| `inputIndex` | number | Position of the row in your input, counting from 0: first the "phones" list, then the items of the dataset. Use it to join the result back to your own rows (duplicates and skipped rows leave gaps). |
| `input` | string | The text exactly as you gave it (cut at 300 characters). |
| `countryHint` | string or null | The country used to read numbers written without "+": the row's own country, else defaultCountry. Null when none was given. A number written with "+" ignores it. |
| `isValid` | boolean | True when the number matches the numbering plan of its country (right length AND a known pattern). This is what the metadata can tell; it does not mean the number is connected to anyone. |
| `isPossible` | boolean | True when the length is possible for the country, even if the number is not valid. A valid number is always possible. |
| `invalidReason` | string or null | Why the number is not valid: NOT_A_NUMBER, CONTAINS_LETTERS, MISSING_COUNTRY (written without "+" and no country known: free of charge, set defaultCountry), MULTIPLE_NUMBERS (several numbers in one cell: free of charge, split them), INVALID_COUNTRY (an unknown calling code), TOO_SHORT, TOO_LONG (also an extension of more than 10 digits), INVALID_LENGTH (a length the country does not use), INVALID_NUMBER (a possible length but no numbering-plan pattern matches). Null when valid. |
| `e164` | string or null | The number in E.164 form, "+" and digits only, without extension. Null when the number is not valid. |
| `international` | string or null | Human-readable international format, with the extension if there is one. Null when not valid. |
| `national` | string or null | Human-readable national format of its own country, with the extension if there is one. Null when not valid. |
| `rfc3966` | string or null | The tel: URI (RFC 3966), with ";ext=" when there is an extension. Null when not valid. |
| `extension` | string or null | The extension found after "ext", "x", "#", ";ext=", "int", "interno", "anexo" or "ramal", digits only. Null when none. |
| `countryCode` | string or null | ISO 3166-1 alpha-2 code of the country the number belongs to (some codes are shared: +1 is US, CA, PR...; the area code decides). Null when the metadata cannot tell, and for non-geographic numbers such as +800. |
| `countryName` | string or null | English name of that country. |
| `callingCode` | number or null | The international calling code as a number (1, 39, 44...). Known even for some invalid numbers; null when the number could not be read at all. |
| `lineType` | string or null | MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE (USA, Canada and others do not tell them apart), TOLL_FREE, PREMIUM_RATE, VOIP, SHARED_COST, PERSONAL_NUMBER, PAGER, UAN, VOICEMAIL, or UNKNOWN (valid but the plan does not say). Deduced from the numbering plan, not looked up live. Null when not valid. |
| `isPremiumOrSpecial` | boolean or null | True for lines that are not ordinary subscriber lines: PREMIUM_RATE, SHARED_COST, UAN, PERSONAL_NUMBER, PAGER, VOICEMAIL. Toll-free and VOIP are not flagged. Null when not valid. |
| `hasLetters` | boolean | True when the number part had letters (a vanity number such as 1-800-FLOWERS). Letters in an extension marker (ext, x) do not count. |
| `vanityConverted` | boolean | True when the letters were turned into digits with the phone keypad (only with convertVanityLetters on). Without it a number with letters is not valid (CONTAINS_LETTERS). |
| `labelStripped` | boolean | True when a label in front of the number ("Tel.", "Phone:", "Mobile", "Cell", "Cellulare", "Telefono") or a note in brackets after it ("(mobile)") was taken off before the number was read. "Fax" is not removed. |
| `matchesCountryHint` | boolean or null | True when the number's calling code is the one of the country hint, false when it is another (a UK number in a column that says Italy). Null when there is no hint or the number could not be read. |
| `normalized` | string or null | The key to deduplicate and join on: the E.164 number, plus ";ext=NN" when it has an extension (two extensions of one switchboard are two different contacts). Null when not valid. |
| `metadataVersion` | string | The library and metadata that produced the row. Numbering plans change; this tells you which snapshot answered. |

**Reasons a number is not valid** (`invalidReason`): `NOT_A_NUMBER`, `CONTAINS_LETTERS`, `MISSING_COUNTRY`, `MULTIPLE_NUMBERS`, `INVALID_COUNTRY`, `TOO_SHORT`, `TOO_LONG`, `INVALID_LENGTH`, `INVALID_NUMBER`.

Two of them are **free of charge** (the row is still delivered, so you can see it and fix it): `MISSING_COUNTRY` (the number is written without `+` and neither the row nor `defaultCountry` gives a country, so the number could not even be tried: this is a missing setting, not a bad number) and `MULTIPLE_NUMBERS` (several numbers in one cell, such as `347 1234567 / 06 1234567`). `INVALID_COUNTRY` is a `+` or `00` number whose calling code does not exist.

**Line types** (`lineType`): `MOBILE`, `FIXED_LINE`, `FIXED_LINE_OR_MOBILE`, `TOLL_FREE`, `PREMIUM_RATE`, `VOIP`, `SHARED_COST`, `PERSONAL_NUMBER`, `PAGER`, `UAN`, `VOICEMAIL`, `UNKNOWN`.

### Real example

This is the output of a real run (not edited) on the numbers below. They are fictitious or public numbers: the `555-01xx` range reserved for fiction, Ofcom's drama range `020 7946 0xxx`, the Vatican switchboard, Google's published example mobile numbers for each country, and a Swiss number from libphonenumber's own documentation examples. The Actor's default country was `IT`.

```json
{
    "phones": [
        { "phone": "(212) 555-0123 x55", "country": "US" },
        "+39 06 6982",
        { "phone": "020 7946 0958", "country": "GB" },
        "0044 20 7946 0958",
        "+1 800 555 0199",
        "+1 900 555 0123",
        { "phone": "312 345 6789", "country": "IT" },
        { "phone": "01512 3456789", "country": "DE" },
        { "phone": "06 12 34 56 78", "country": "FR" },
        { "phone": "612 34 56 78", "country": "ES" },
        { "phone": "(11) 96123-4567", "country": "BR" },
        { "phone": "081234 56789", "country": "IN" },
        { "phone": "0412 345 678", "country": "AU" },
        { "phone": "222 123 4567", "country": "MX" },
        { "phone": "06 12345678", "country": "NL" },
        { "phone": "090-1234-5678", "country": "JP" },
        "+41 44 668 18 00",
        { "phone": "07400 123456", "country": "UK" },
        { "phone": "555-0123", "country": "US" },
        { "phone": "347 1234567", "country": "US" },
        "+999 12345",
        { "phone": "1-800-FLOWERS", "country": "US" },
        "not a phone",
        "",
        null,
        "+39 06 6982 interno 12",
        "12345",
        "+39 347 1234567 1234567890",
        "39 312 345 6789",
        "+800 1234 5678"
    ],
    "defaultCountry": "IT"
}
```

Result for each of the 30 entries (`—` means `null`; the rows themselves have all the fields above):

| # | Input | Country | Valid | Type / reason | E.164 | ISO |
|---|---|---|---|---|---|---|
| 0 | `(212) 555-0123 x55` | US | yes | FIXED_LINE_OR_MOBILE | `+12125550123` | US |
| 1 | `+39 06 6982` | IT | yes | FIXED_LINE | `+39066982` | VA |
| 2 | `020 7946 0958` | GB | yes | FIXED_LINE | `+442079460958` | GB |
| 4 | `+1 800 555 0199` | IT | yes | TOLL_FREE | `+18005550199` | US |
| 5 | `+1 900 555 0123` | IT | yes | PREMIUM_RATE | `+19005550123` | US |
| 6 | `312 345 6789` | IT | yes | MOBILE | `+393123456789` | IT |
| 7 | `01512 3456789` | DE | yes | MOBILE | `+4915123456789` | DE |
| 8 | `06 12 34 56 78` | FR | yes | MOBILE | `+33612345678` | FR |
| 9 | `612 34 56 78` | ES | yes | MOBILE | `+34612345678` | ES |
| 10 | `(11) 96123-4567` | BR | yes | MOBILE | `+5511961234567` | BR |
| 11 | `081234 56789` | IN | yes | MOBILE | `+918123456789` | IN |
| 12 | `0412 345 678` | AU | yes | MOBILE | `+61412345678` | AU |
| 13 | `222 123 4567` | MX | yes | FIXED_LINE_OR_MOBILE | `+522221234567` | MX |
| 14 | `06 12345678` | NL | yes | MOBILE | `+31612345678` | NL |
| 15 | `090-1234-5678` | JP | yes | MOBILE | `+819012345678` | JP |
| 16 | `+41 44 668 18 00` | IT | yes | FIXED_LINE | `+41446681800` | CH |
| 17 | `07400 123456` | GB | yes | MOBILE | `+447400123456` | GB |
| 18 | `555-0123` | US | no | TOO_SHORT | — | US |
| 19 | `347 1234567` | US | no | INVALID_NUMBER | — | US |
| 20 | `+999 12345` | IT | no | INVALID_COUNTRY | — | — |
| 21 | `1-800-FLOWERS` | US | no | CONTAINS_LETTERS | — | — |
| 22 | `not a phone` | IT | no | CONTAINS_LETTERS | — | — |
| 25 | `+39 06 6982 interno 12` | IT | yes | FIXED_LINE | `+39066982` | VA |
| 26 | `12345` | IT | no | TOO_SHORT | — | IT |
| 27 | `+39 347 1234567 1234567890` | IT | no | TOO_LONG | — | — |
| 29 | `+800 1234 5678` | IT | yes | TOLL_FREE | `+80012345678` | — |

Things to notice in this table:

- Entries 3 and 28 are **not in the table**: `0044 20 7946 0958` is the same number as entry 2, and `39 312 345 6789` is the same as entry 6, so they were **skipped for free** (`dedupe` is on by default). Entries 23 and 24 are empty (`""` and `null`) and also free.
- `+39 06 6982` is reported as **`VA`** (Vatican City): in libphonenumber's numbering plan, `+39 06 698…` belongs to the Vatican, not to Italy, although both use calling code 39.
- A number with `+` keeps its own country whatever the default country says: `+1 800 555 0199` is `US` even though the default country is `IT`; its `matchesCountryHint` is `false`, which is how you spot a UK number in a column that says Italy.
- `347 1234567` read as a US number is `INVALID_NUMBER`: the length is possible, but no US pattern matches. Read as an Italian number (the default country) it is a valid mobile.
- `+800 1234 5678` is an international toll-free number that belongs to no country: `countryCode` is `null` and `callingCode` is `800`.

A valid row, with an extension (exactly what the dataset contains):

```json
{
    "inputIndex": 0,
    "input": "(212) 555-0123 x55",
    "countryHint": "US",
    "isValid": true,
    "isPossible": true,
    "invalidReason": null,
    "e164": "+12125550123",
    "international": "+1 212 555 0123 ext. 55",
    "national": "(212) 555-0123 ext. 55",
    "rfc3966": "tel:+12125550123;ext=55",
    "extension": "55",
    "countryCode": "US",
    "countryName": "United States",
    "callingCode": 1,
    "lineType": "FIXED_LINE_OR_MOBILE",
    "isPremiumOrSpecial": false,
    "hasLetters": false,
    "vanityConverted": false,
    "labelStripped": false,
    "matchesCountryHint": true,
    "normalized": "+12125550123;ext=55",
    "metadataVersion": "libphonenumber-js@1.13.15 (max metadata, from Google libphonenumber)"
}
```

A valid mobile number whose country came from the row's own `country`:

```json
{
    "inputIndex": 6,
    "input": "312 345 6789",
    "countryHint": "IT",
    "isValid": true,
    "isPossible": true,
    "invalidReason": null,
    "e164": "+393123456789",
    "international": "+39 312 345 6789",
    "national": "312 345 6789",
    "rfc3966": "tel:+393123456789",
    "extension": null,
    "countryCode": "IT",
    "countryName": "Italy",
    "callingCode": 39,
    "lineType": "MOBILE",
    "isPremiumOrSpecial": false,
    "hasLetters": false,
    "vanityConverted": false,
    "labelStripped": false,
    "matchesCountryHint": true,
    "normalized": "+393123456789",
    "metadataVersion": "libphonenumber-js@1.13.15 (max metadata, from Google libphonenumber)"
}
```

A number that is not valid (`isPossible` is `true`: the length is fine, the pattern is not):

```json
{
    "inputIndex": 19,
    "input": "347 1234567",
    "countryHint": "US",
    "isValid": false,
    "isPossible": true,
    "invalidReason": "INVALID_NUMBER",
    "e164": null,
    "international": null,
    "national": null,
    "rfc3966": null,
    "extension": null,
    "countryCode": "US",
    "countryName": "United States",
    "callingCode": 1,
    "lineType": null,
    "isPremiumOrSpecial": null,
    "hasLetters": false,
    "vanityConverted": false,
    "labelStripped": false,
    "matchesCountryHint": true,
    "normalized": null,
    "metadataVersion": "libphonenumber-js@1.13.15 (max metadata, from Google libphonenumber)"
}
```

The **run summary** (`SUMMARY` record of the key-value store) has only counts, never a number. This is the one of the run above, without the two time stamps:

```json
{
    "status": "finished",
    "stopReason": null,
    "statusNotice": null,
    "settings": {
        "defaultCountry": "IT",
        "dedupe": true,
        "onlyValid": false,
        "onlyNew": false,
        "convertVanityLetters": false,
        "maxItems": null,
        "seenStoreName": null,
        "source": {
            "phones": 30,
            "dataset": false
        }
    },
    "metadata": {
        "library": "libphonenumber-js",
        "version": "1.13.15",
        "metadata": "max",
        "description": "libphonenumber-js@1.13.15 (max metadata, from Google libphonenumber)"
    },
    "totals": {
        "rows": 30,
        "looked": 30,
        "emptyOrNotText": 2,
        "duplicates": 2,
        "alreadySeen": 0,
        "notDelivered": 0,
        "alreadyDelivered": 0,
        "delivered": 26,
        "deliveredFree": 0,
        "notProcessed": 0,
        "valid": 19,
        "invalid": 7,
        "uncharged": 0
    },
    "results": {
        "valid": 19,
        "invalid": 7,
        "withExtension": 2,
        "vanityConverted": 0,
        "countryHintMismatch": 4
    },
    "byCountry": {
        "US": 3,
        "GB": 2,
        "VA": 2,
        "+800": 1,
        "AU": 1,
        "BR": 1,
        "CH": 1,
        "DE": 1,
        "ES": 1,
        "FR": 1,
        "IN": 1,
        "IT": 1,
        "JP": 1,
        "MX": 1,
        "NL": 1
    },
    "byLineType": {
        "MOBILE": 10,
        "FIXED_LINE": 4,
        "FIXED_LINE_OR_MOBILE": 2,
        "TOLL_FREE": 2,
        "PREMIUM_RATE": 1
    },
    "byInvalidReason": {
        "CONTAINS_LETTERS": 2,
        "TOO_SHORT": 2,
        "INVALID_COUNTRY": 1,
        "INVALID_NUMBER": 1,
        "TOO_LONG": 1
    },
    "billing": {
        "event": "phone",
        "billableRows": 26,
        "note": "One event per delivered row, except the free ones: rows without a country to read them with (MISSING_COUNTRY), cells with several numbers (MULTIPLE_NUMBERS), empty rows, duplicates, rows skipped by onlyNew and invalid rows left out by onlyValid."
    },
    "warnings": [
        "2 row(s) were empty or not text and were skipped for free. Send phone numbers as text: a JSON number loses its leading zeros."
    ]
}
```

### How to use it

#### Input

| Field | What it does |
|---|---|
| `phones` | A list of phone numbers, as text. An entry can also be an object `{"phone": "...", "country": "IT"}` when that row has its own country. Send numbers as **text**, not as JSON numbers (a JSON number loses its leading zeros: such entries are skipped for free and counted). |
| `datasetId`, `phoneField`, `countryField` | To clean a list that is already in Apify: the ID or name of one of your datasets, the field that holds the phone (a path such as `contact.phone` works) and, optionally, the field with the country of each item. The dataset must already exist (a missing one stops the run before it does anything, and is never created); it is read page by page and never changed. You can use `phones` and a dataset in the same run: the list is read first. |
| `defaultCountry` | ISO 3166-1 alpha-2 code (`IT`, `US`, `GB`…) used for numbers written **without** `+` or an international prefix, when the row has no country of its own. `UK` is accepted for `GB`. A number with `+` always keeps its own country. **Without a country** (no `defaultCountry`, no country on the row) a number written without `+` comes back `MISSING_COUNTRY`: it is delivered, **free of charge**, and the run says so in a warning and in the status message. |
| `dedupe` | Default `true`: the same number written in different ways is delivered and charged **once**; the repetitions are free. Numbers that differ only by extension are different numbers. `false`: every row gets a result. |
| `onlyValid` | Default `false`. `true`: only valid numbers are delivered and charged; the invalid ones are only counted (by reason) in the summary, for free. |
| `onlyNew` | Default `false`. `true`: numbers delivered by an earlier run with the same memory are skipped for free. See below. |
| `seenStoreName` | Name of the key-value store used by `onlyNew` (default `phone-validator-seen-hashes`). Use one name per list or campaign. |
| `convertVanityLetters` | Default `false`: a number with letters is not valid (`CONTAINS_LETTERS`). `true`: if the number part has 3 or more letters, all between A and Z, they are turned into digits with the phone keypad (`1-800-FLOWERS` becomes `+1 800 356 9377`) and the row says `vanityConverted: true`. |
| `maxItems` | Stop after this many **charged** results have been delivered (free rows do not count). Empty = no limit. The spending limit of the run is respected as well. |

A row is read like this: the row's own `country` if it has a valid one, otherwise `defaultCountry`. A row whose `country` is not an ISO code gets **no** country (it is never silently read as another one): numbers with `+` still work, national numbers get `MISSING_COUNTRY` (free), and the summary counts these rows in a warning.

What is accepted in a number: spaces, dots, dashes, slashes and brackets; a leading `+` or `00` (or the international prefix of the default country, such as `011` in the USA); full-width and Arabic-Indic digits; a `tel:` prefix; extensions written `ext`, `ext.`, `extension`, `x`, `#`, `;ext=`, `int`, `interno`, `anexo`, `ramal` at the end (up to 10 digits; a longer one is `TOO_LONG`).

**Labels and notes are taken off before the number is read:** a label in front (`Tel.`, `Phone:`, `Mob`, `Mobile`, `Cell`, `Cellulare`, `Telefono`) and a note in brackets after it (`(mobile)`, `(ufficio)`). The row says so with `labelStripped: true`; `input` stays as you gave it. Nothing is removed when no digit would be left. `Fax` is **not** a label: a fax number is not a number to call or text, so `Fax 06 1234567` stays `CONTAINS_LETTERS`.

**Two numbers in one cell** (`A / B` with spaces around the slash, `A, B` or `A; B`) are not guessed at: the row is `MULTIPLE_NUMBERS`, free of charge. Put one number in each cell.

About `00`: libphonenumber only understands the international prefix of the *default country*. When a number written with `00` does not work as written, the Actor tries it as `+`, so `0044 20 7946 0958` works with any default country. A number that already works is never rewritten.

#### Output

The dataset has the 22 fields above, in two table views (**Clean list** and **Audit**). `inputIndex` is the position of the row in your input (counting the `phones` list first, then the dataset): use it to join the results back to your own rows, remembering that duplicates and empty rows leave gaps. `input` is the text exactly as you gave it, cut at 300 characters.

Results are saved to the dataset in batches of 1000 rows, and the charge for a batch is made **right after** the rows are saved, so you are never charged for rows you do not have.

#### Only numbers not seen before (`onlyNew`)

For recurring jobs (a new export every week), `onlyNew` skips the numbers you already got. The memory is a **named key-value store in your own account**. It keeps only **salted SHA-256 hashes** of the normalized numbers – never a number in clear. The salt is random, made once per store, so the hashes are meaningless outside that store. A hash of a phone number is still *pseudonymous* personal data (anyone who knows the salt can test a guess), so treat the store like your other data: delete it to forget everything. Numbers are remembered only when their row was delivered. Two runs that use the same store at the same time can overwrite each other's memory; run them one after the other.

### How much does it cost?

One event, **`phone`: $0.0005 per number** that gets a result – valid, or not valid with the reason. That is **$0.50 per 1,000 numbers**.

| Numbers | Cost |
|---|---|
| 1,000 | $0.50 |
| 10,000 | $5.00 |
| 100,000 | $50.00 |
| 1,000,000 | $500.00 |

**You are not charged for:** empty rows and entries that are not text, duplicates (with `dedupe`), numbers skipped by `onlyNew`, and – with `onlyValid` – the invalid numbers that are not delivered. Two more kinds of rows **are delivered but not charged**, because the Actor could not try to read them: `MISSING_COUNTRY` (written without `+` and no country known: set `defaultCountry`) and `MULTIPLE_NUMBERS` (several numbers in one cell). They are counted in the run summary (`totals.deliveredFree`, `byInvalidReason`, `billing.billableRows`), and the run adds a warning that says what to set; if more than half of the first numbers have no country, the status message of the run says it in capital letters. The run respects the **maximum cost per run** you set: it stops before the limit, delivers nothing beyond it, and says so in the summary. Platform usage is included in the price.

The example above: 30 entries, 26 results charged = $0.013; 2 duplicates and 2 empty entries cost nothing.

### Speed and memory (measured)

Measured on a laptop, Node 22, offline analysis, 21 October 2026: **50,000 numbers analysed in about 0.65 seconds** (roughly 75,000 per second, including the duplicate check). A complete local run of 50,000 mixed numbers – 44,837 rows written to local storage after 5,163 duplicates were left out – took 10 seconds and peaked at 204 MB. A complete local run of 100,000 distinct numbers took about 22 seconds in three runs and peaked at 200, 202 and 274 MB (the peak moves with the timing of the memory clean-up). On the platform the analysis is not the bottleneck: the time is set by the dataset writes and the charges, two Apify API calls per batch of 1000 rows.

The duplicate check keeps one short key in memory per distinct number: about 160 bytes each. Measured peak: 241 MB for 1,000,000 distinct numbers and 523 MB for 3,000,000. The Actor starts with **512 MB**, about twice the highest peak measured for 100,000 numbers; for several hundred thousand numbers or more, start it with 1 GB or more.

### Limits to know

- The metadata is a **snapshot**: numbering plans change. Every row carries `metadataVersion` (the library version that answered). New versions of this Actor update the library.
- Some calling codes are shared between countries (+1 for the USA, Canada and the Caribbean; +39 for Italy and the Vatican; +44 for the UK, Guernsey, Jersey and the Isle of Man; +7 for Russia and Kazakhstan). `countryCode` is the country libphonenumber assigns from the number itself.
- A very long list in `phones` is limited by the maximum size of an Apify input: for large lists put them in a dataset and use `datasetId`.
- Only the first 300 characters of `input` are echoed back.
- Letters are never guessed: without `convertVanityLetters`, text like `call me 555 0100` is `CONTAINS_LETTERS`; with it, only a number with at least 3 letters from A to Z is converted.
- The Actor never writes a phone number in the run log; numbers appear only in your dataset.

### FAQ

**Is "valid" the same as "this number works"?** No. It means the number fits the numbering plan of its country. It can still be unassigned, disconnected or a landline that does not take SMS. Only a live (HLR) lookup can tell, and this Actor does not do one.

**Why is a mobile number reported as `FIXED_LINE_OR_MOBILE`?** In the USA, Canada, Mexico and a few other countries the numbering plan does not separate the two. The honest answer is "can be either".

**Why does my Italian number say `VA`?** Numbers `+39 06 698…` are assigned to Vatican City. They are valid Italian-dialling numbers with country code `VA`.

**A number I know is good comes back `INVALID_NUMBER`.** Check that the country is right (`matchesCountryHint`) and that the number is complete. Very new ranges can be missing from an older snapshot: `metadataVersion` tells you which one answered.

**Why `MISSING_COUNTRY`, and why was I not charged?** The number was written without `+` (for example `347 1234567`) and no country was known, so it could not be read: a number means different things in different countries. Nothing is wrong with the number, so you are not charged. Set `defaultCountry` (for example `IT`) or send a country with each row, and run again.

**Why is `e164` empty for an invalid number?** To stop an unchecked number from slipping into an import. Use `input`, `callingCode` and `invalidReason` to fix it; `normalized` and the formatted fields exist only for valid numbers.

**How does `dedupe` decide that two numbers are the same?** By the E.164 plus the extension. `+39 347 123 4567`, `0039 3471234567` and `347-123-4567` (default country `IT`) are one number. Invalid entries are compared by their digits and country hint.

**Can I clean a CSV or an Excel file?** Upload it with any Apify CSV/Excel importer (or paste the column into `phones`), then export the dataset. Keep the column as text so that leading zeros survive.

**Is it GDPR-safe?** Phone numbers are personal data and you decide what to send. The Actor processes them in memory, writes them only to your dataset, and never logs them. It makes no network request about the numbers.

### Use it from code

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('far003/phone-number-validator-e164').call({
    phones: ['(212) 555-0123 x55', { phone: '020 7946 0958', country: 'GB' }, '+39 06 6982'],
    defaultCountry: 'IT',
    onlyValid: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.map((row) => [row.e164, row.lineType]));
```

```python
from apify_client import ApifyClient

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("far003/phone-number-validator-e164").call(run_input={
    "datasetId": "<your dataset id>",
    "phoneField": "phone",
    "countryField": "country",
    "defaultCountry": "US",
    "dedupe": True,
})
rows = client.dataset(run["defaultDatasetId"]).list_items().items
```

### Credits and licences

Numbering-plan metadata: Google's [libphonenumber](https://github.com/google/libphonenumber) (Apache License 2.0), through [libphonenumber-js](https://gitlab.com/catamphetamine/libphonenumber-js) (MIT), version 1.13.15. This Actor is not affiliated with, or endorsed by, Google.

# Actor input Schema

## `phones` (type: `array`):

The numbers to check. Each entry is either a text such as "+39 347 123 4567" or an object {"phone": "347 123 4567", "country": "IT"} when this row has its own country (ISO 3166-1 alpha-2). Send numbers as text, not as JSON numbers (a number loses its leading zeros). Spaces, dots, dashes, brackets, "00" or "+" prefixes and extensions (ext, x, #) are fine. Empty and non-text entries are skipped for free. Use this list, a dataset (below), or both.

## `datasetId` (type: `string`):

To clean a list that is already in Apify, give the ID or the name of one of your datasets here and say which field holds the phone number. The dataset is only read.

## `phoneField` (type: `string`):

Name of the field with the phone number in each dataset item. A path such as "contact.phone" is accepted. Required with a dataset ID.

## `countryField` (type: `string`):

Name of the field with the country of each item (ISO 3166-1 alpha-2 such as "IT"). When it is empty or missing for an item, the default country is used.

## `defaultCountry` (type: `string`):

Two-letter ISO 3166-1 country code (IT, US, GB, DE...) used to read numbers written without "+" or an international prefix, when the row has no country of its own. A number that starts with "+" always keeps its own country. Leave empty only if all your numbers are international: a number written without "+" and with no country is free of charge and comes back as MISSING_COUNTRY, so nothing is read.

## `convertVanityLetters` (type: `boolean`):

Off (default): a number with letters is reported as not valid (CONTAINS_LETTERS). On: when the number part has 3 or more letters and nothing else than A-Z, they are turned into digits with the phone keypad (1-800-FLOWERS becomes 1-800-356-9377) and the row says so (`vanityConverted`).

## `dedupe` (type: `boolean`):

On (default): the same number written in different ways (+39 347 123 4567, 0039 3471234567...) is delivered and charged once; the repetitions are free. Numbers with different extensions are different numbers. Off: every row gets its own result.

## `onlyValid` (type: `boolean`):

On: only numbers that are valid are delivered and charged; the others are counted in the run summary and cost nothing. Off (default): invalid numbers are delivered too, with the reason, so you can fix them.

## `onlyNew` (type: `boolean`):

On: numbers delivered by an earlier run that used the same memory are skipped for free. The memory is a key-value store in your account that keeps only salted SHA-256 hashes of the numbers, never a number. Off (default): no memory is read or written.

## `seenStoreName` (type: `string`):

Name of the key-value store used by "Only new". Use one name per list or campaign. 3 to 63 characters: lowercase letters, digits and hyphens. Delete the store to forget everything.

## `maxItems` (type: `integer`):

Stop after this many charged results have been delivered (free rows, such as MISSING_COUNTRY, do not count). Leave empty for no limit. Rows not reached are listed as a count in the run summary. The spending limit of the run is always respected as well.

## Actor input object example

```json
{
  "phones": [
    "+1 (212) 555-0123 x55",
    {
      "phone": "06 6982",
      "country": "VA"
    },
    "020 7946 0958"
  ],
  "datasetId": "aBcD1234eFgH5678",
  "phoneField": "phone",
  "countryField": "country",
  "defaultCountry": "IT",
  "convertVanityLetters": false,
  "dedupe": true,
  "onlyValid": false,
  "onlyNew": false,
  "seenStoreName": "phone-validator-seen-hashes",
  "maxItems": 10000
}
```

# Actor output Schema

## `numbers` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "phones": [
        "+1 (212) 555-0123 ext. 55",
        "+39 06 6982",
        {
            "phone": "020 7946 0958",
            "country": "GB"
        },
        "0044 20 7946 0958",
        "+1 800 555 0199",
        "+1 900 555 0123",
        "12345",
        "+39 06 6982"
    ],
    "convertVanityLetters": false,
    "dedupe": true,
    "onlyValid": false,
    "onlyNew": false,
    "seenStoreName": "phone-validator-seen-hashes"
};

// Run the Actor and wait for it to finish
const run = await client.actor("far003/phone-number-validator-e164").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "phones": [
        "+1 (212) 555-0123 ext. 55",
        "+39 06 6982",
        {
            "phone": "020 7946 0958",
            "country": "GB",
        },
        "0044 20 7946 0958",
        "+1 800 555 0199",
        "+1 900 555 0123",
        "12345",
        "+39 06 6982",
    ],
    "convertVanityLetters": False,
    "dedupe": True,
    "onlyValid": False,
    "onlyNew": False,
    "seenStoreName": "phone-validator-seen-hashes",
}

# Run the Actor and wait for it to finish
run = client.actor("far003/phone-number-validator-e164").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "phones": [
    "+1 (212) 555-0123 ext. 55",
    "+39 06 6982",
    {
      "phone": "020 7946 0958",
      "country": "GB"
    },
    "0044 20 7946 0958",
    "+1 800 555 0199",
    "+1 900 555 0123",
    "12345",
    "+39 06 6982"
  ],
  "convertVanityLetters": false,
  "dedupe": true,
  "onlyValid": false,
  "onlyNew": false,
  "seenStoreName": "phone-validator-seen-hashes"
}' |
apify call far003/phone-number-validator-e164 --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,far003/phone-number-validator-e164"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/QSUxxiTbb7pVe2ert/builds/wex9ApL1t47o9CHE8/openapi.json
