# Phone Number Validator - Bulk E.164, Mobile vs Landline (`lukehunter/phone-number-validator`) Actor

Offline phone number validator and E.164 formatter for bulk lists. Valid or not, E.164, national and international format, country and mobile or landline type, from libphonenumber. No API key, no lookups. $0.001 per valid number; invalid and duplicate numbers are free.

- **URL**: https://apify.com/lukehunter/phone-number-validator.md
- **Developed by:** [Luke Hunter](https://apify.com/lukehunter) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 valid 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 - Bulk E.164, Mobile vs Landline

**A bulk phone number validator and E.164 formatter for sales, marketing and CRM teams, running entirely offline.** Paste a list of phone numbers in any common format, or hand it the dataset of a contact scraper, and get one clean row per number: **valid or not, E.164, international and national format, country, calling code and line type (mobile or landline, toll-free, premium rate, VoIP and more)**. It uses Google's libphonenumber metadata, the same data behind Android and many CRMs.

**$0.001 per valid number. Invalid, unparseable and duplicate numbers are free. 1,000 numbers that are all valid = $1.**

There is no network call, no carrier lookup, no API key and no third-party service, so it is instant (100,000 numbers take seconds), has no upstream that can break, and no phone number ever leaves the run.

> **Read this before you rely on it.** Offline validation proves a number is **valid in its country's numbering plan**: right length, an allocated range, a known line type. It does **not** prove the number is currently connected, that anyone answers it, or who owns it. There is no live "is this phone reachable" check and no carrier or ported-number lookup. If you need that, you need a paid HLR service; this Actor is the cheap first pass that removes the numbers that can never work.

### What you get

| Field | Meaning |
|---|---|
| `input` | The number exactly as you gave it (cut at 200 characters). |
| `index` | Position in the run's list, from 1. Rows are written in batches of 1,000, valid first, so sort by `index` to restore your order. |
| `status` | `valid`, `invalid` (readable but not a real number) or `unparseable` (not a phone number, or no country known). |
| `valid` | `true` when the number is valid in its national numbering plan. |
| `possible` | `true` when the length is plausible for the country, even if the number is not valid. |
| `e164` | International E.164 form, e.g. `+442079460000`. Valid numbers only. |
| `international`, `national` | Formatted as `+44 20 7946 0000` and `020 7946 0000`. Valid numbers only. |
| `countryCode`, `callingCode` | ISO-2 country (`GB`, `US`, `DE`, `IM`...) and calling code (`44`). |
| `numberType` | `MOBILE`, `FIXED_LINE`, `FIXED_LINE_OR_MOBILE`, `TOLL_FREE`, `PREMIUM_RATE`, `SHARED_COST`, `VOIP`, `PERSONAL_NUMBER`, `PAGER`, `UAN`, `VOICEMAIL`. |
| `extension` | An extension written after the number (`x123`, `ext. 5`), kept out of `e164`. |
| `duplicateOf` | When the same E.164 and extension appeared earlier in the run, the `index` of the first. Duplicates are free. |
| `reason` | For invalid and unparseable rows: why (too short, no country, not numbered in this country). |
| `source` | For numbers read from rows or a dataset: `{row, field, domain}`, to join results back. |

### Quick start

```json
{
  "phoneNumbers": ["+44 20 7946 0000", "(202) 555-0123", "0151 496 0123"],
  "defaultCountry": "GB",
  "maxItems": 10
}
```

Numbers that start with `+` (or `00`) carry their own country. Numbers without it, such as `0151 496 0123`, use `defaultCountry`; without a default they come back `unparseable` with a reason that says so. Spaces, dots, dashes, brackets, `(0)`, `tel:` prefixes and extensions are all fine.

### Recipes

**Clean phone numbers from scraped leads before importing into a CRM.** Chain the dataset of [Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper), [Google Maps Email Extractor & Lead Enricher](https://apify.com/lukehunter/lead-enricher) or [Impressum Scraper](https://apify.com/lukehunter/impressum-scraper) straight in, with no export step:

```json
{
  "datasetId": "<DEFAULT_DATASET_ID_OF_THE_CONTACT_SCRAPER_RUN>",
  "defaultCountry": "GB",
  "maxItems": 5000
}
```

The Actor reads `phones` (a list of texts or of `{phone}` objects, as the contact scraper and lead enricher write them), or `phone` (Impressum Scraper, the contact scraper's contacts output, Google Maps rows). A row's own `countryCode` (for example `DE` on a Google Maps row) overrides `defaultCountry` for that row's numbers. Import only rows with `status` = `valid` and use `e164` as the phone column; `source.row` and `source.domain` tell you which lead each number came from. Rows without a phone are skipped and counted.

**Split mobile vs landline for SMS campaigns.** Run the list, then filter the dataset on `numberType`: send SMS to `MOBILE`, call or skip `FIXED_LINE`, and drop `PREMIUM_RATE`, `SHARED_COST` and `TOLL_FREE` from outreach lists. In the United States, Canada and some other countries the numbering plan cannot tell mobile from landline: those come back as `FIXED_LINE_OR_MOBILE`, so treat them as unknown, not as mobile.

**Normalise to E.164.** E.164 (`+` country code and number, no spaces, no extension) is what Twilio, WhatsApp, Google Contacts and most CRMs want. Give any mix of formats and a `defaultCountry`, and take the `e164` column:

```json
{
  "phoneNumbers": ["020 7946 0000", "+44 (0)20 7946 0018", "0044 20 7946 0000", "(202) 555-0123"],
  "defaultCountry": "GB"
}
```

The three UK forms give `+442079460000` (twice) and `+442079460018`; the repeat is marked `duplicateOf` and not charged, so the list de-duplicates itself. The US number is invalid under `GB`: write it as `+1 202 555 0123` or give it its own `countryCode` in `rows`.

**Check a column from a spreadsheet.** Paste the column into `phoneNumbers` (one per line, or separated by commas or semicolons) and download the dataset as CSV or Excel.

### Input

| Field | Default | What it does |
|---|---:|---|
| `phoneNumbers` | none | Phone numbers in any common format. |
| `defaultCountry` | none | ISO-2 country for numbers without a `+`, e.g. `GB`, `US`, `DE`. `UK` is accepted for `GB`. |
| `rows` | none | Objects with a phone field, merged with `phoneNumbers`. |
| `datasetId` | none | An Apify dataset whose items have a phone field (up to 100,000 items are read). |
| `phoneField` | auto | Field name holding the number(s). Empty tries `phones`, `phone`, `phoneNumber`, `phoneNumbers`, `phone_number`, `telephone`, `tel`, `mobile`, `phoneUnformatted`. |
| `maxItems` | `1000` | Hard cap on numbers checked per run, 1 to 100,000. |

At least one of `phoneNumbers`, `rows` or `datasetId` is required. Numbers over `maxItems` are not checked, and the run's status message says how many.

### Example output

```json
{
  "input": "020 7946 0000",
  "index": 1,
  "status": "valid",
  "valid": true,
  "possible": true,
  "e164": "+442079460000",
  "international": "+44 20 7946 0000",
  "national": "020 7946 0000",
  "countryCode": "GB",
  "callingCode": "44",
  "numberType": "FIXED_LINE",
  "extension": null,
  "duplicateOf": null,
  "reason": null,
  "source": { "row": 3, "field": "phones", "domain": "example.co.uk" }
}
```

A number that is the right length but not allocated is `invalid` and free:

```json
{
  "input": "+91 22 1234 5678",
  "index": 2,
  "status": "invalid",
  "valid": false,
  "possible": true,
  "e164": null,
  "international": null,
  "national": null,
  "countryCode": "IN",
  "callingCode": "91",
  "numberType": null,
  "extension": null,
  "duplicateOf": null,
  "reason": "Right length, but not a number that exists in the national numbering plan.",
  "source": null
}
```

The example numbers are from public documentation and reserved ranges (Ofcom's drama range `020 7946 0xxx`, the US `555-01xx` range), not real subscribers. Status meanings:

- `valid`: a number that exists in its national numbering plan. **Charged**, unless it is a duplicate.
- `invalid`: readable as a number but not valid (wrong length, or an unallocated range). Free.
- `unparseable`: not a phone number, empty, too long, or no country known. Free.

### Use with AI agents (MCP)

This Actor works as a tool for Claude, ChatGPT, Cursor and other MCP clients through [Apify's MCP server](https://mcp.apify.com). Every input property description is short and carries an example, so agents fill it in correctly. Give it `phoneNumbers`, `rows` or a `datasetId`.

**Claude Desktop** (`claude_desktop_config.json`), with your own token from Apify Console > Settings > API & Integrations:

```json
{
  "mcpServers": {
    "apify-phone-number-validator": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://mcp.apify.com?tools=lukehunter/phone-number-validator",
        "--header", "Authorization: Bearer ${APIFY_TOKEN}"
      ],
      "env": { "APIFY_TOKEN": "<YOUR_APIFY_TOKEN>" }
    }
  }
}
```

Clients that support OAuth (Claude.ai, VS Code) can connect with just the URL `https://mcp.apify.com?tools=lukehunter/phone-number-validator` and sign in to Apify. Never paste a real token into a shared file or chat.

**Example.** You ask: "Is +44 20 7946 0000 a valid UK number, and is it a mobile or a landline?" The agent calls `lukehunter--phone-number-validator` with:

```json
{
  "phoneNumbers": ["+44 20 7946 0000"],
  "maxItems": 1
}
```

It answers `valid`, `FIXED_LINE` (a landline), and is charged $0.001 for the one valid number.

### Pricing

Pay-per-event: **$0.001 per valid number** (event `number`). Invalid numbers, unparseable numbers and duplicates of an earlier number in the same run are free, and nothing is charged for reading a dataset.

| Valid numbers | Cost |
|---:|---:|
| 10 | $0.01 |
| 1,000 (default cap) | $1.00 |
| 10,000 | $10.00 |
| 100,000 (maximum) | $100.00 |

For comparison, other phone validation Actors on the Apify Store were listed in October 2026 at between $0.001 and $0.06 per number. The ones at about $0.015 and above generally bundle live carrier or reachability lookups, which this Actor does not do; the one at the same $0.001 returns no line type, region or national format. `maxItems` and your Apify per-run spending limit both stop delivery before the cap is passed: a result is delivered and charged together, and never past your limit. Price changes follow Apify's notice rules; the Pricing tab always shows the live price.

### Limitations

- **Valid is not reachable.** The check is against the numbering plan, so a valid number may be unassigned, disconnected, a landline nobody answers, or ported to another carrier. For live reachability or carrier lookups you need a different, paid service.
- **Line type comes from the numbering plan.** `numberType` is the type the country's plan assigns to that range. It is not live: a mobile number ported to a VoIP provider still shows `MOBILE`. Countries whose plan does not separate mobile from fixed (the United States and Canada among them) return `FIXED_LINE_OR_MOBILE`, and some ranges have no type at all (`numberType` is `null`).
- **Country is where the range was allocated.** `countryCode` is the country or territory the range belongs to; for shared codes it picks the territory (for example `+44 7624` is `IM`, Isle of Man). Non-geographic numbers (such as `+800`) have no country.
- **Needs a country to read a national number.** Without a `+` and without `defaultCountry` (or a row `countryCode`), a number like `020 7946 0000` is `unparseable`. The Actor never guesses a country from the digits.
- **Numbering plans change.** The metadata is Google's libphonenumber data bundled at build time; new ranges allocated since the last release show as `invalid` until the Actor is rebuilt with newer data.
- **Personal data.** Phone numbers can identify a person. The Actor processes them only for the run, writes them only to your dataset, never contacts anyone, and logs only counts. Handle the data in line with the GDPR and your local rules, including any consent rules for calls and SMS.
- **Not legal advice.** A valid, mobile number is not permission to message it.

### FAQ

**Does it call or text the numbers?** No. It never contacts anyone and never touches the network. It only reads the number's digits against libphonenumber's rules.

**Is it a phone validation API?** Yes, in the way Apify Actors are: call it from code, Make, Zapier or n8n with the API snippets above and get JSON back. See "Use it from your code" below.

**How can I tell mobile or landline?** Read `numberType`: `MOBILE` or `FIXED_LINE`. Where it says `FIXED_LINE_OR_MOBILE`, the country's plan cannot say.

**Why was a number I know is real marked `invalid`?** Usually a missing digit, a stray digit, a trunk `0` in the wrong place, or a very new range. `reason` says which kind of problem it is. If you are sure the number works and it is not a brand-new range, open an issue with the number's country and format.

**Are duplicates charged?** No. The same E.164 (with the same extension) appearing again later in the run is returned with `duplicateOf` set to the first row's `index` and is free.

**What about extensions?** `+44 20 7946 0000 x123` gives `e164` `+442079460000` and `extension` `123`. The base number is what is validated and charged.

**Which countries are covered?** Every country and territory with a calling code in libphonenumber, about 240.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~phone-number-validator/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"phoneNumbers": ["+44 20 7946 0000", "+1 (202) 555-0123", "+1 800 555 0199"], "maxItems": 10}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~phone-number-validator').call({
  "phoneNumbers": [
    "+44 20 7946 0000",
    "+1 (202) 555-0123",
    "+1 800 555 0199"
  ],
  "maxItems": 10
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~phone-number-validator').call(run_input={"phoneNumbers": ["+44 20 7946 0000", "+1 (202) 555-0123", "+1 800 555 0199"], "maxItems": 10})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Domain Email Provider Lookup](https://apify.com/lukehunter/email-provider-mx-lookup)**: the email provider (Google Workspace, Microsoft 365, Zoho and more), MX, SPF and DMARC setup of any domain list, with provider-change alerts.
- **[French Company Leads](https://apify.com/lukehunter/french-companies-sirene)**: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
- **[Poland KRS Company Lookup](https://apify.com/lukehunter/poland-krs-company-lookup)**: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
- **[EU VAT Number Validator](https://apify.com/lukehunter/eu-vat-number-validator)**: bulk EU VAT number checks against the official VIES service: valid or invalid, registered name and address, with optional consultation numbers.
- **[Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper)**: emails, phone numbers and social profile links from any list of company domains, with the page each came from and a changed-contacts-only mode for scheduled runs.
- **[Impressum Scraper](https://apify.com/lukehunter/impressum-scraper)**: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.
- **[Website Tech Stack Detector](https://apify.com/lukehunter/website-tech-stack-detector)**: the technologies a website runs (CMS, ecommerce platform, analytics, ad pixels, chat, payments, CDN, frameworks) from its home page, with the evidence for each match and a changed-technologies-only mode for scheduled runs.
- **[Google Maps Email Extractor & Lead Enricher](https://apify.com/lukehunter/lead-enricher)**: emails, phones, social links, named people and the tech stack of every business in your Google Maps Scraper results, one enriched row per business, with filters such as only Shopify stores with an email.

# Actor input Schema

## `phoneNumbers` (type: `array`):

Phone numbers in any common format, e.g. \["+44 20 7946 0000", "(202) 555-0123", "0151 496 0123"]. Spaces, dots, dashes, brackets and extensions ("x123") are fine. Numbers without a + prefix use defaultCountry. Invalid and unparseable numbers are free. Max 100,000 per run.

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

Two-letter ISO country code used for numbers written without a + prefix, e.g. "GB", "US", "DE". Numbers that start with + always use their own country code. Without it, a national number like "020 7946 0000" cannot be read and comes back unparseable. "UK" is accepted for GB.

## `rows` (type: `array`):

Objects that carry phones, e.g. \[{"domain": "example.com", "phones": \["+44 20 7946 0000"]}]. Read from phoneField, or from phones, phone, phoneNumber, telephone, tel, mobile or phoneUnformatted. A row's countryCode (e.g. "DE") overrides defaultCountry for its numbers. Rows without a phone are skipped. Merged with phoneNumbers.

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

ID of an Apify dataset whose items have a phone field, to chain runs, e.g. the default dataset of a Website Contact Scraper, Lead Enricher or Impressum Scraper run (fields phones or phone). Read at the start of the run, up to 100,000 items. Merged with phoneNumbers and rows.

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

Field that holds the phone number(s) in rows or the dataset, e.g. "phones" (a list of texts or of {phone} objects) or "phone". Leave empty to try phones, phone, phoneNumber, phoneNumbers, phone_number, telephone, tel, mobile and phoneUnformatted in that order.

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

Hard cap on numbers checked per run, 1 to 100,000, e.g. 1000. Only valid numbers are charged, at $0.001 each, so 1,000 numbers cost at most $1. Numbers beyond the cap are not checked and the run's status message says how many.

## Actor input object example

```json
{
  "phoneNumbers": [
    "+44 20 7946 0000",
    "+1 (202) 555-0123",
    "+1 800 555 0199"
  ],
  "maxItems": 10
}
```

# Actor output Schema

## `numbers` (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 = {
    "phoneNumbers": [
        "+44 20 7946 0000",
        "+1 (202) 555-0123",
        "+1 800 555 0199"
    ],
    "maxItems": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/phone-number-validator").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 = {
    "phoneNumbers": [
        "+44 20 7946 0000",
        "+1 (202) 555-0123",
        "+1 800 555 0199",
    ],
    "maxItems": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/phone-number-validator").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 '{
  "phoneNumbers": [
    "+44 20 7946 0000",
    "+1 (202) 555-0123",
    "+1 800 555 0199"
  ],
  "maxItems": 10
}' |
apify call lukehunter/phone-number-validator --silent --output-dataset

```

## MCP server setup

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

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/bfJTcQMvqELyQSm9V/builds/apNTumgheUe079Yl3/openapi.json
