# Bulk Phone Number Validator - Format, Type & Country (`tidytools/phone-validator`) Actor

Validate and format phone numbers in bulk, offline: valid or not, E.164 / international / national format, country, line type (mobile, fixed line, toll free, VoIP) and time zones. CSV upload or another Actor's dataset. Invalid numbers are free. $0.80 per 1,000.

- **URL**: https://apify.com/tidytools/phone-validator.md
- **Developed by:** [Yukai Lin](https://apify.com/tidytools) (community)
- **Categories:** Lead generation, Marketing, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.64 / 1,000 phone number validateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### What does Bulk Phone Number Validator do?

**Phone number validator and formatter** for lists you are about to import into a CRM, a dialer or an SMS tool: give it a list, a CSV or the dataset of a lead scraper and get, for every number, **valid or not**, the **E.164, international, national and RFC 3966 formats**, the **country**, the **line type** (mobile, fixed line, toll free, VoIP…) and the **reason** when it is invalid. **$0.80 per 1,000 valid numbers, no start fee, invalid numbers free.**

It is an **offline check** against the official numbering plans (the metadata of Google's libphonenumber, through the MIT-licensed libphonenumber-js). Nothing is dialled, texted or looked up at a carrier, so it is fast (10,000 numbers in seconds), private and cheap. The trade-off, stated honestly: `valid` means the number belongs to an assigned number range of its country, **not** that the line is currently active or who the carrier is (`liveCheck` is always `false`).

- ✅ **Valid / possible** per country: length and number-range rules for 240+ countries and territories
- 🔢 **Four formats**: E.164 (`+886912345678`), international (`+886 912 345 678`), national (`0912 345 678`), RFC 3966 (`tel:+886912345678`); pick the one that goes into the `phone` column
- 🌍 **Country**, ISO code and calling code, also for shared codes such as +1 (US, Canada, Caribbean) and +44 (UK, Jersey, Guernsey, Isle of Man)
- 📱 **Line type**: `MOBILE`, `FIXED_LINE`, `FIXED_LINE_OR_MOBILE` (US/Canada cannot tell them apart), `TOLL_FREE`, `PREMIUM_RATE`, `VOIP`, `PERSONAL_NUMBER`, `PAGER`, `SHARED_COST`, `UAN`, `VOICEMAIL`
- ☎️ **Extensions** (`ext. 12`, `x12`, `#12`, `;ext=12`), `tel:` links, `00` / full-width digits, labels such as `Phone:` and **vanity letters** (`1-800-FLOWERS`) are understood
- 🕒 **Time zones of the country** (country level, e.g. all 29 US zones; not per area code)
- 📥 **Any list shape**: a list field, pasted text, a **CSV upload** (the phone column is found automatically; other columns can be copied to the output) or the **dataset of another Actor** (e.g. Contact Details Extractor's `phones`)
- 🔁 **Duplicates merged** on the E.164 form: `0912 345 678` (default country TW) and `+886 912 345 678` count once
- 📤 **Clean list**: the run also saves `VALID_NUMBERS` as plain text, one valid number per line in your chosen format

### Why a number is invalid

| `reasonCode` | Meaning |
|---|---|
| `INVALID_COUNTRY` | No `+country code` and no "Default country" set, or the country code does not exist |
| `TOO_SHORT` / `TOO_LONG` / `INVALID_LENGTH` | The length does not fit any number of that country |
| `NOT_VALID_FOR_REGION` | Right length, but no assigned number range starts like this (e.g. US area code 123) |
| `NOT_A_NUMBER` | Not readable as a phone number |
| `MULTIPLE_NUMBERS` | Several `+` numbers in one entry that could not be split; put each on its own line |

An invalid number has no `e164`, `type` or `countryCode` (the country is not guessed; with a "Default country" and no `+`, that country is kept). Short codes such as `911` or `112` are reported as too short: this is a check of full phone numbers.

Entries with fewer than 3 digits, e-mail addresses and links get a row with `errorType: "invalid_input"` and are not charged.

### How much does it cost?

| Event | Free / Bronze | Silver | Gold and above |
|---|---|---|---|
| Phone number validated | **$0.0008** ($0.80 per 1,000) | $0.00072 | $0.00064 |

**No start fee.** Charged: each unique number that is valid for its country (`valid: true`). **Not charged:** invalid numbers, entries that are not phone numbers (fewer than 3 digits, e-mail addresses, links), and duplicates merged on the E.164 form. Every row says `charged: true/false`.

#### How it compares

Checked October 2026 on the Apify Store (search "phone number validator", "phone validator", "validate phone numbers"), prices on the Free plan from the Store API. Users are the Store's 30-day counts.

| Actor | Does | Price on the Free plan | 30-day users |
|---|---|---|---|
| **This Actor** | Offline format, country, line type, time zone; CSV and dataset input | **$0.0008 per valid number; no start fee; invalid free** | new |
| memo23/phone-enrichment | Offline validation plus US reverse lookup | $0.001 per number + $0.005 start | 31 |
| pink_comic/phone-number-validation | Validation with carrier lookup | $0.001 per result | 19 |
| scrapersdelight/phone-number-validator-scraper | Validation, carrier and line type | $0.001 per number | 10 |
| automation-lab/phone-number-validator | Validation | $0.00115 per number + $0.0115 start | 9 |
| santamaria-automations/phone-validator | International validation | $0.001 per result + $0.001 start | 4 |
| khadinakbar/phone-number-lookup-api | Carrier and line-type lookup | $0.025 per number | 48 |
| checkthatphone/phone-validator | US/Canada carrier lookup and TCPA litigator scrub | $0.015 per credit | 70 |

What this one is: a fast offline format check, priced below the other offline validators (and with no start fee), with invalid numbers and duplicates free, CSV and dataset input and a stop at your maximum charge per run. What it is not: a carrier or live-line check. If you need the carrier name, ported-number status, a reverse lookup or TCPA screening, the tools above that do lookups do something this one deliberately does not.

### Control your cost

At the start the run logs its plan, e.g. `Plan: 10 valid numbers × $0.0008 = at most $0.008`. It never charges more than your **maximum charge per run**: when the limit is reached, the remaining valid numbers are not processed and are listed in `SUMMARY.notProcessed` (invalid numbers are still reported, free). "Max numbers" caps how many unique numbers one run processes. A run restarted by Apify skips numbers it already charged.

### How to use it

1. Paste numbers into **Phone numbers** (or paste a list, upload a CSV, or give a dataset ID).
2. Set **Default country** for numbers written without `+country code` (e.g. `US` for `(650) 253-0000`, `TW` for `0912 345 678`).
3. Choose the **format** of the `phone` column (E.164 is best for CRMs and SMS tools) and run.

#### Input example

```json
{
    "phones": ["+1 650-253-0000", "+44 20 7946 0958 ext. 12", "0912 345 678", "(02) 2345-6789", "+1 123 456 7890"],
    "defaultCountry": "TW",
    "outputFormat": "E164"
}
```

#### Output example (local test run, 2 October 2026)

```json
{
    "input": "0912 345 678",
    "valid": true,
    "possible": true,
    "phone": "+886912345678",
    "e164": "+886912345678",
    "international": "+886 912 345 678",
    "national": "0912 345 678",
    "rfc3966": "tel:+886912345678",
    "countryCode": "TW",
    "country": "Taiwan",
    "callingCode": "+886",
    "nationalNumber": "912345678",
    "type": "MOBILE",
    "extension": null,
    "timezones": ["Asia/Taipei"],
    "defaultCountryUsed": "TW",
    "reasonCode": null,
    "liveCheck": false,
    "charged": true,
    "source": "list",
    "duplicates": 2
}
```

An invalid number from the same run (not charged):

```json
{ "input": "+1 123 456 7890", "valid": false, "possible": true, "phone": null, "callingCode": "+1", "reasonCode": "NOT_VALID_FOR_REGION", "reason": "right length, but no assigned number range of this country code starts like this", "charged": false }
```

The key-value store holds `SUMMARY` (counts by line type, country and invalid reason; `notProcessed` when the run stopped at your limit) and `VALID_NUMBERS`.

### Integrations

Run it after a lead or contact scraper: set **Or read from a dataset** to that run's dataset ID (the `phones` field is found automatically) and copy fields such as `url` or `name` with **Copy these source columns**. Pairs with Email List Cleaner to clean both columns of a lead list.

### Use with AI agents (MCP)

Connect Apify's MCP server (https://mcp.apify.com?tools=tidytools/phone-validator) to Claude, Cursor or any MCP client, then ask e.g. "Validate these phone numbers and give me the mobile ones in E.164: …".

```json
{ "phones": ["+1 650-253-0000", "0912 345 678"], "defaultCountry": "TW" }
```

Invalid numbers are not charged and carry a `reasonCode`; entries that are not phone numbers carry `errorType: "invalid_input"`.

### Tips and limitations

- **Not a live check.** No HLR, carrier or "line in use" lookup. Ported numbers keep their original range, so the line type is the type of the range.
- **US and Canada** numbers are `FIXED_LINE_OR_MOBILE`: the numbering plan does not separate them.
- **Time zones** are per country, not per area code.
- The numbering metadata is updated with each libphonenumber-js release; this Actor pins the version in its build.

### Responsible use

This Actor works offline: numbers are checked inside the run against numbering-plan rules, nothing is dialled, texted or sent to a carrier or to any outside service, and the results stay in your own Apify run storage. Use the results to clean and format contact data you are allowed to process. Validating a number does not give consent to call or text it: telemarketing and SMS rules (e.g. TCPA in the US, GDPR/ePrivacy in the EU, do-not-call registers) still apply.

### FAQ

**Does the Actor collect usage data?** The Actor sends one anonymous usage counter per run (input type, item count range, outcome) to improve the tool; no URLs, content or account data.

**Does "valid" mean the number is in use?** No. It means the number belongs to an assigned number range of its country. The Actor does not call, text or query carriers, so it cannot tell whether a line is active or who the carrier is today.

**What happens to numbers without a country code?** They are read with your **Default country**. Without one, they are invalid with `reasonCode: "INVALID_COUNTRY"` (and free).

**How are lists split, and what about numbers inside text?** New lines, semicolons, tabs, pipes, `/` and `and` / `or` / `&` between numbers separate entries; a comma separates only when every part has 6+ digits. A number inside a sentence ("call +1 650 253 0000 today") is found and validated; a sentence with no number is `NOT_A_NUMBER`. Vanity letters are converted only in entries that start with a digit, `+` or `(` and have at most 8 digits ("1-800-FLOWERS").

**Is the same number with a different extension a duplicate?** No: `+1 650 253 0000 x12` and `+1 650 253 0000 x13` are two entries (each charged if valid); the same number and extension written two ways is one.

**Can I keep the other columns of my CSV or dataset?** Yes: list them in **Copy these source columns to the output** (for example `name`, `url`), and each result row carries them next to the phone fields.

**I only want the clean list.** Turn on **Leave invalid numbers out of the results**, or download `VALID_NUMBERS` from the key-value store: one valid number per line in your chosen format.

**What if my input is empty or has no phone numbers?** The run still ends normally (it does not fail): you get one row with `errorType: "invalid_input"`, `charged: false` and a message saying what to add, and `SUMMARY.status` is `NO_RESULTS`. Nothing is charged.

# Actor input Schema

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

One number per line (use Bulk edit to paste a whole list). Any common spelling works: +1 (650) 253-0000, 0044 20 7946 0958, tel: links, "ext. 12". Numbers without a +country code need "Default country". Entries with fewer than 3 digits get a free error row.

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

Two-letter country code (ISO 3166, e.g. US, GB, DE, TW) used for numbers written without a +country code, such as "(650) 253-0000" or "0912 345 678". Empty = such numbers are reported invalid (free) with reason "no country". Numbers with a + are always read with their own country code.

## `outputFormat` (type: `string`):

Every row has all four formats; this one also goes into the "phone" column and the clean list VALID_NUMBERS.

## `phonesText` (type: `string`):

Numbers separated by new lines, semicolons, tabs or " / ", e.g. a column copied from a spreadsheet. Spaces inside a number are fine.

## `csvFile` (type: `string`):

A CSV or TSV file (up to 50 MB), or a link to one (e.g. a Google Sheets "Publish to the web" CSV link). The column named like "phone" or "mobile" is used, or the column with the most numbers; set "CSV column" to choose. Excel files: save as CSV first.

## `csvColumn` (type: `string`):

Header name (e.g. "Mobile Phone") or column number (1 = first) of the numbers in the CSV file. Empty = detect automatically.

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

ID of a dataset from another run, e.g. Contact Details Extractor or a lead scraper. Every number in the chosen field is validated (fields holding several numbers, such as a "phones" array, are expanded).

## `datasetField` (type: `string`):

Field with the numbers (dot paths allowed, e.g. "contact.phone"). Empty = the first field named like phones/phone/phoneNumber, or the field whose values look like phone numbers.

## `keepFields` (type: `array`):

For CSV and dataset input: column or field names to copy into each row's sourceFields (e.g. "name", "company"), so the result maps back to your list.

## `skipInvalid` (type: `boolean`):

On: the dataset holds valid numbers only (invalid ones are still counted in SUMMARY). Off: every input gets a row, with the reason when it is invalid. Invalid numbers are free either way.

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

Validate (and charge) each number once, comparing the E.164 form, so "0912 345 678" and "+886 912 345 678" count as one. The row gets duplicates = how many more times it appeared. Turn off to get one row per input line.

## `maxPhones` (type: `integer`):

Cost cap: at most this many unique numbers are processed per run ($0.80 per 1,000 valid numbers on the Free plan). The rest are listed in SUMMARY.notProcessed.

## Actor input object example

```json
{
  "phones": [
    "+1 650-253-0000",
    "+44 20 7946 0958 ext. 12",
    "+49 30 901820",
    "0912 345 678",
    "(02) 2345-6789",
    "+1 800 555 0199",
    "+1 123 456 7890",
    "+44 20 7946"
  ],
  "defaultCountry": "TW",
  "outputFormat": "E164",
  "keepFields": [],
  "skipInvalid": false,
  "dedupe": true,
  "maxPhones": 100000
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `validNumbers` (type: `string`):

No description

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

No description

## `full` (type: `string`):

No description

## `allColumns` (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 650-253-0000",
        "+44 20 7946 0958 ext. 12",
        "+49 30 901820",
        "0912 345 678",
        "(02) 2345-6789",
        "+1 800 555 0199",
        "+1 123 456 7890",
        "+44 20 7946"
    ],
    "defaultCountry": "TW"
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidytools/phone-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 = {
    "phones": [
        "+1 650-253-0000",
        "+44 20 7946 0958 ext. 12",
        "+49 30 901820",
        "0912 345 678",
        "(02) 2345-6789",
        "+1 800 555 0199",
        "+1 123 456 7890",
        "+44 20 7946",
    ],
    "defaultCountry": "TW",
}

# Run the Actor and wait for it to finish
run = client.actor("tidytools/phone-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 '{
  "phones": [
    "+1 650-253-0000",
    "+44 20 7946 0958 ext. 12",
    "+49 30 901820",
    "0912 345 678",
    "(02) 2345-6789",
    "+1 800 555 0199",
    "+1 123 456 7890",
    "+44 20 7946"
  ],
  "defaultCountry": "TW"
}' |
apify call tidytools/phone-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tidytools/phone-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/lIC2MOymH7l4MlpCH/builds/ecXj7Px6WiEG6Xqwn/openapi.json
