# Bulk Phone Number Validator & Formatter (Offline) (`divinxxii/phone-validator`) Actor

Validate, format (E.164 / international / national) and classify phone numbers in bulk with Google libphonenumber. Offline, no data leaves Apify.

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

## Pricing

from $0.56 / 1,000 number checkeds

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

## Phone Number Validator & Formatter (Bulk, Offline)

This phone number validator checks, formats and classifies phone numbers in bulk. Paste a list or point it at a CSV and get **E.164, international and national formats**, the **number type** (mobile, fixed line, VoIP, toll-free, premium rate…), **region**, **timezones** and **carrier name** (where Google's data has it) for every number, straight from Google's libphonenumber rules.

**Fully offline.** No third-party API, no lookups, no scraping. The numbers you submit are checked by a local copy of the libphonenumber rules, so they never leave your Apify run. That makes it fast, cheap and private: **$0.80 per 1,000 numbers**.

### Why use this phone number validator

| | This actor | Lookup / enrichment actors |
|---|---|---|
| Data source | Local libphonenumber rules (Apache-2.0) | Third-party carrier / HLR / web lookups |
| Your numbers sent to a third party | **No** | Often yes |
| Speed | Thousands of numbers per second | Limited by remote lookups |
| Cost | $0.005 start + $0.0008 per number | Varies: compare their per-number price |
| Live line status (is the phone switched on?) | No | Sometimes |

If you need to know whether a line is *currently live* (HLR lookup), this is not that tool. If you need to clean, standardise and classify a list (CRM import, SMS list hygiene, dialler prep, dedupe), this is the cheap and private way to do it. It is an offline alternative to lookup-based actors such as memo23/phone-enrichment and automation-lab/phone-number-validator.

### What the phone number formatter returns

`original`, `valid`, `possible`, `possibleReason`, `e164`, `international`, `national`, `countryCallingCode`, `regionCode`, `location`, `numberType`, `carrier`, `timezones`, `extension`, `duplicateOf`, and, only on rows re-written after a restart, `recoveredAfterRestart`.

### Input

| Field | Description |
|---|---|
| `numbers` | List of numbers in any common format |
| `text` | Free-text blob, one number per line (or `;` separated) |
| `csvUrl` + `csvColumn` | Public CSV URL, read as a stream (up to 200 MB). Column by header name or 1-based index; auto-detects headers like "phone" / "mobile" / "tel" |
| `defaultCountry` | ISO code (e.g. `GB`, `US`, `DE`) used for numbers written without `+` country code. Default `GB` |
| `dedupe` | Flag repeats (compared by E.164) with `duplicateOf`; duplicates are **free**. Default on |

Run it with no input and you get a 10-number sample.

#### Example input

```json
{
    "numbers": ["+44 20 7946 0958", "07911 123456", "+1 (650) 253-0000", "not a number"],
    "defaultCountry": "GB",
    "dedupe": true
}
```

#### Example output

```json
{
    "original": "+44 20 7946 0958",
    "valid": true,
    "possible": true,
    "possibleReason": "IS_POSSIBLE",
    "e164": "+442079460958",
    "international": "+44 20 7946 0958",
    "national": "020 7946 0958",
    "countryCallingCode": 44,
    "regionCode": "GB",
    "location": "London",
    "numberType": "FIXED_LINE",
    "carrier": null,
    "timezones": ["Europe/London"],
    "extension": null,
    "duplicateOf": null
}
```

Invalid inputs are still returned, with `valid: false` and the reason in `possibleReason` (`NOT_A_NUMBER`, `TOO_SHORT`, `INVALID_COUNTRY_CODE`…), so you can match results back to your source rows.

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.005 | Once per run |
| Number checked | $0.0008 | Per phone number checked, valid or not (duplicates free) |

| Numbers | Approximate cost |
|---|---|
| 1,000 | $0.805 |
| 10,000 | $8.005 |
| 100,000 | $80.005 |

- **Actor start:** $0.005 per run (Apify's `apify-actor-start` event). Maximum run memory is capped at 1,024 MB so the start event is never multiplied.
- **Number checked:** $0.0008 per number (`number-checked` event). Charged for every input that is checked, valid or not.
- Duplicates flagged by `dedupe` are not charged.

Example: 100,000 numbers ≈ $80.00 + $0.005. The run stops cleanly if you set a maximum cost per run.

### Performance

Numbers are processed as a stream in batches of 1,000, and progress is saved to the key-value store (`PROGRESS`) so a restart or migration continues where it left off. Results are never lost and never double-charged. In rare cases a platform restart leaves one batch whose outcome is unknown; that batch is written again without charge, so you may see duplicate rows, and every such row carries `recoveredAfterRestart: true` so you can de-duplicate (drop the flagged row if the number also appears unflagged). Measured end to end on a Windows dev machine (Python 3.11, local file storage, no pricing configured): 20,000 numbers took 66 s with a peak of about 182 MB resident memory (5,000 numbers: 21 s, 180 MB, so memory is dominated by the interpreter and libphonenumber metadata, not by input size). Validation alone runs at roughly 3,400 numbers/s; the rest is dataset writes. A 100,000-number run is projected at about 5-6 minutes and under 256 MB; this projection has not been run on the Apify platform, where dataset write speed will differ.

### Data source, licence and compliance

- **Source:** the [`phonenumbers`](https://github.com/daviddrysdale/python-phonenumbers) Python port of Google's [libphonenumber](https://github.com/google/libphonenumber). Both are licensed under **Apache-2.0** (verified 2026-10-21 against the `LICENSE` file in the python-phonenumbers repository). Apache-2.0 permits commercial use and redistribution with attribution. The bundled metadata (formats, carrier prefixes, timezones, geocoding) is part of the same project.
- **ToS / robots.txt:** not applicable to the validation itself. The actor makes **no network requests** to any data provider, and nothing is scraped. The only network access is when *you* supply a `csvUrl`: the actor then fetches that file, identifies itself with a User-Agent, and honours the host's `robots.txt` (it refuses to fetch if disallowed, and also if robots.txt cannot be fetched or returns a 5xx error; every redirect target is checked again, private/internal addresses are blocked, and requests are spaced about 1 second apart; this cannot be switched off).
- Credentials in URLs (`user:pass@host`) are not sent: the actor never makes authenticated requests, so use a URL that needs no login.
- **Attribution:** powered by Google libphonenumber via python-phonenumbers (Apache-2.0).
- **Personal data:** phone numbers are supplied by you. The actor does not store them beyond the run's dataset and key-value store (which you control and can delete), makes no external calls with them, and logs no numbers, CSV row contents or full URLs. You are responsible for having a lawful basis (e.g. UK GDPR) for processing the numbers you submit.
- **Monitoring / "changed since" mode:** not applicable, as there is no external source that changes. Output is deterministic for a given library version.

### Limitations

- Checks numbers against **numbering plans and formats**, not whether a line is live or who owns it. "Valid" means the number is allocated within a valid range for its country.
- **Carrier** is the original allocation holder and is only available for some countries; it is empty for many (including GB, where number portability makes prefix data unreliable). It is never a live lookup.
- Number type can be `FIXED_LINE_OR_MOBILE` or `UNKNOWN` where the plan does not distinguish them.
- Numbers must be one per row/line; commas are not used as separators in `text` (use new lines or `;`).
- Rules follow the bundled libphonenumber release and change as new versions are published.
- CSV input: quoted multi-line fields are parsed as one record. `csvColumn` takes a header name or a 1-based number; `csvHeader` (`auto`/`yes`/`no`) says whether the first row is a header, so a headerless file with `csvColumn: 2` keeps its first row. The `PROGRESS` key-value record holds only counters, never numbers.

### FAQ

**How do I validate phone numbers in bulk?**
Add them to `numbers`, paste them into `text`, or give a public `csvUrl`. Set `defaultCountry` for numbers written without a `+` prefix. Each number becomes one result row.

**Can it format phone numbers to E.164?**
Yes. Every row has `e164`, `international` and `national` formats. Invalid numbers keep `valid: false` and a reason in `possibleReason`.

**Does it tell mobile from landline and VoIP numbers?**
It returns `numberType` (mobile, fixed line, VoIP, toll-free, premium rate and more) from the libphonenumber numbering plans. It does not check whether a line is currently switched on.

**Are my numbers sent to a third party?**
No. Validation runs offline inside your Apify run.

### Related Actors

- [Email Validator & List Cleaner](https://apify.com/divinxxii/email-list-cleaner): clean an email list alongside your phone list (MX, disposable domains, typos).
- [PDF to Markdown for RAG](https://apify.com/divinxxii/doc-to-markdown): turn PDF, DOCX, PPTX and HTML into Markdown for LLMs.
- [Domain Health Checker](https://apify.com/divinxxii/domain-health-audit): SPF, DMARC, DKIM and SSL expiry checks for any domain.

# Actor input Schema

## `numbers` (type: `array`):

List of phone numbers in any common format. Leave empty (with no text or CSV URL) to run a small built-in sample.

## `text` (type: `string`):

Paste numbers separated by new lines or semicolons (e.g. a column copied from a spreadsheet).

## `csvUrl` (type: `string`):

Public URL of a CSV file. The actor reads one column, streaming, and honours the host's robots.txt.

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

Header name, or 1-based column number (1 is the first column; 0 is rejected), holding the phone numbers. If empty, the first header containing 'phone', 'mobile', 'tel', 'cell', 'msisdn' or 'number' is used; with no such header, column 1 is read and the first row is treated as data.

## `csvHeader` (type: `string`):

Whether the first CSV row is a header (skipped). 'auto': it is a header if any of its cells looks like a phone header (phone, mobile, tel, cell, msisdn, number). 'yes': always skip it. 'no': always treat it as data (not allowed with a header-name column).

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

ISO 3166-1 alpha-2 code used for numbers written without a +country prefix, e.g. GB, US, DE, FR. Leave empty to require international format.

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

Flag repeated numbers (compared by E.164 form) with duplicateOf pointing at the first occurrence. Duplicates are not charged.

## Actor input object example

```json
{
  "numbers": [
    "+44 20 7946 0958",
    "07911 123456",
    "+1 (650) 253-0000",
    "+49 30 901820",
    "not a number"
  ],
  "csvHeader": "auto",
  "defaultCountry": "GB",
  "dedupe": true
}
```

# Actor output Schema

## `results` (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 = {
    "numbers": [
        "+44 20 7946 0958",
        "07911 123456",
        "+1 (650) 253-0000",
        "+49 30 901820",
        "not a number"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("divinxxii/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 = { "numbers": [
        "+44 20 7946 0958",
        "07911 123456",
        "+1 (650) 253-0000",
        "+49 30 901820",
        "not a number",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("divinxxii/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 '{
  "numbers": [
    "+44 20 7946 0958",
    "07911 123456",
    "+1 (650) 253-0000",
    "+49 30 901820",
    "not a number"
  ]
}' |
apify call divinxxii/phone-validator --silent --output-dataset

```

## MCP server setup

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