# Bulk Phone Number Validator & Cleaner: Carrier, CSV or Sheet (`nerolabs/phone-number-validator`) Actor

Bulk phone validation for an Apify dataset, CSV/Excel/JSON file or Google Sheet. Returns every original row plus country, line type, mobile or landline, region, carrier lookup, timezone and E.164. Inputs: datasetId or fileUrl, phoneField, defaultRegion. Charged per number. Agent-ready: x402, MCP.

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

## Pricing

from $1.40 / 1,000 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.
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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Phone Number Validator & Cleaner (Dataset, CSV or Sheet)

Half your phone list will never connect. Numbers typed without a country code, landlines sitting in a column you are about to text, premium-rate switchboards, the same person entered twice in two different formats, and rows where somebody wrote "call the office" instead of a number.

Point this Actor at the list you already have. It hands back **every original column, untouched**, with the verdict added alongside: is the number real, which country it belongs to, whether it is a mobile or a landline, where it is, which network it was issued on, its timezone, and its clean E.164 form ready for your dialler or SMS provider.

### What it does not do, up front

This Actor checks numbers **offline**, against Google's own libphonenumber data. That means it can tell you a number is correctly formed and exists in its country's numbering plan. It **cannot** tell you:

- whether the line is currently connected, or whether anyone answers it
- which network the number sits on **today**, if it has been ported since it was issued

Both of those need a paid live network lookup (HLR or CNAM) against a third-party service, and this Actor does not perform one. So every row it returns carries `lineChecked: false`, and the summary repeats it. A number marked `valid` passed every check performed here. That is not the same as a guarantee it will ring.

Anything advertising "live" carrier or connection status is calling a paid service behind the scenes and charging you for it. That is a real product, it is just a different one, and it usually costs 25 to 50 times more per number.

### What makes this different from the other phone validators

Every other phone validator on the Store takes a **typed-in list of numbers** and gives you back a **bare list of results**. If your numbers live in a scraped dataset, a CRM export or a Google Sheet, you have to pull the phone column out, paste it in, and then match the results back to your names and companies by hand.

This one takes the whole thing. An Apify dataset, a CSV, TSV, Excel or JSON file by URL, a Google Sheet, or rows pasted inline. Your columns come back with the verdict columns added, in the same order, ready to use.

Two other things worth knowing:

- **Duplicates are matched on the real number, not the text.** `020 7946 0123`, `+44 20 7946 0123` and `+442079460123` are one number, and this Actor says so. Matching on the raw string, which is what the alternatives do, misses all three.
- **It says "I cannot tell" instead of guessing.** See the mobile question in the FAQ.

### What you get on every row

| Column | What it is |
|---|---|
| `phoneStatus` | `valid`, `risky`, `invalid` or `duplicate` |
| `phoneReason` | The single most important thing found, for example `premium_rate` or `unknown_region` |
| `phoneAllReasons` | Everything that applied, so you can set your own bar |
| `phoneE164` | `+442079460123`, the format every SMS and telephony API wants |
| `phoneNational` / `phoneInternational` | `020 7946 0123` and `+44 20 7946 0123`, for display |
| `phoneCountry` | ISO country code, worked out from the number itself |
| `phoneType` | `mobile`, `fixed_line`, `fixed_line_or_mobile`, `toll_free`, `premium_rate`, `voip`, `shared_cost`, `uan`, `pager`, `voicemail` |
| `isMobile` | `true`, `false`, or `null` where the numbering plan genuinely cannot say |
| `phoneRegion` | A place, where the plan carries one: `London`, `New York, NY`, `Manchester` |
| `phoneCarrier` | The network the range was **allocated** to, for example `Vodafone` or `Optus` |
| `phoneTimezones` | IANA timezones, so a dialling list can respect local hours |
| `phoneExtension` | Kept separately when the input had one, for example `ext 22` |
| `lineChecked` | Always `false`. See above. |

### Example

Input, eight rows of a lead list with `Default region` set to `GB`:

| name | company | phone |
|---|---|---|
| Ana Silva | Silva Ltd | +44 7825 123456 |
| Ben Okafor | Okafor & Co | 0161 496 0123 |
| Cara Lind | Lind Studio | +1 212 555 0123 |
| Dan Reeves | Reeves Ltd | +44 900 123 4567 |
| Eve Mbeki | Mbeki Group | +61 412 345 678 |
| Frank Ortiz | Ortiz SA | call the office |
| Gina Haas | Haas Studio | +44 56 0000 0000 |
| Ana Silva (again) | Silva Ltd | 07825 123456 |

What comes back:

| phone | phoneStatus | phoneReason | phoneE164 | phoneType | isMobile | phoneRegion | phoneCarrier |
|---|---|---|---|---|---|---|---|
| +44 7825 123456 | valid | ok | +447825123456 | mobile | true | | Vodafone |
| 0161 496 0123 | valid | ok | +441614960123 | fixed\_line | false | Manchester | |
| +1 212 555 0123 | valid | ok | +12125550123 | fixed\_line\_or\_mobile | *null* | New York, NY | |
| +44 900 123 4567 | risky | premium\_rate | +449001234567 | premium\_rate | false | | |
| +61 412 345 678 | valid | ok | +61412345678 | mobile | true | | Optus |
| call the office | invalid | not\_a\_number | | unknown | *null* | | |
| +44 56 0000 0000 | risky | voip\_number | +445600000000 | voip | false | | |
| 07825 123456 | duplicate | duplicate\_in\_list | +447825123456 | mobile | true | | Vodafone |

Four valid, two risky, one invalid, one duplicate. Note the last row: written completely differently from the first, and correctly recognised as the same person's number.

### Typical uses

- **Before an SMS campaign.** Set `Rows to keep` to "Only confirmed mobile numbers" and you get a list you can actually text, with landlines, switchboards and repeats already gone.
- **Cleaning a scraped lead list.** Run it straight on the dataset your scraper produced, no copying columns about.
- **Keeping a CRM tidy on a schedule.** Point it at a Google Sheet, set a webhook, and let it report in whenever something breaks.
- **Working out what you actually have.** The summary breaks the list down by country, line type and verdict before you spend anything on it.

### Pricing

`$2.00 per 1,000 numbers checked` ($0.002 each), plus one cent per exported file and two cents per webhook delivery.

- A 1,000-row list: **$2.00**
- A 5,000-row list with a CSV and an Excel file: **$10.02**
- A nightly 300-row check with a webhook: **$0.62 a night**, about $19 a month

Every number that gets a verdict is charged, including ones you filter out of the output with `Rows to keep`, because the work is the same. Numbers are never charged twice for the same row, and a run that fails before any checking charges nothing but the run start.

Store discount tiers are on from day one, so Bronze, Silver and Gold plan customers pay 10, 20 and 30 percent less.

### FAQ

**Why does it say `null` for mobile on my US and Canadian numbers?**
Because no offline database can answer that question for North America. The United States and Canada draw mobile and landline numbers from the same ranges, so there is no pattern to read. Reporting `null` is the honest answer, and it is the reason `Rows to keep: mobile` excludes them: it gives you numbers that are definitely mobile, not numbers that might be. Countries with separate mobile ranges (the UK, Australia, Germany, India, most of the world) come back as a clear `true` or `false`. The `undeterminedMobileCount` in the summary tells you how many rows this affected.

**Is `phoneCarrier` the network the number is on now?**
No, and this matters. It is the network the number's **range was originally allocated to**. Number portability means a number issued on one network can have been carried to another years ago, and no offline data can see that. Treat it as a strong hint, not a fact. It is also unavailable for the United States and Canada entirely.

**My test number came back invalid, is it broken?**
Probably not. Regulators reserve certain ranges for use in films and adverts, and some of them exclude those ranges from the real numbering plan. UK numbers starting `07700 900` are the common one: they are deliberately not assignable, so they validate as invalid, correctly. US `555-01xx` numbers, by contrast, are still technically valid. If you want a realistic test, use a real number.

**Why does a UK mobile sometimes come back as country `GG` or `IM`?**
Guernsey, Jersey and the Isle of Man share the `+44` country code with the UK but have their own ranges and their own ISO codes. A number reported as `GG` is genuinely a Guernsey number. It is not a mistake, and the number still works.

**What happens to numbers with no country code?**
Set `Default region` to the country they belong to (`GB`, `US`, `AU`) and they are read correctly. Without it, a number like `0161 496 0123` could belong to a dozen countries, so it is reported as `unknown_region` rather than guessed at. If your list mixes countries and the numbers have no country codes, there is no way for any tool to resolve that, and this one will tell you rather than pretend.

**Does it work on a Google Sheet?**
Yes. Set sharing to "Anyone with the link can view" and paste the normal sheet link into `File URL`. The export link is worked out for you.

**Is my data sent anywhere?**
No. Every check runs inside your own Actor run against bundled data. Nothing is sent to a third-party service. The only outbound request the Actor ever makes is downloading your file, if you gave it a `File URL`, and posting the summary to your own webhook, if you set one. The webhook payload carries counts and download links, never the numbers themselves.

**Can an AI agent run this?**
Yes. It is pay-per-event with limited permissions and no standby mode, so it is payable through x402 and callable over MCP.

### The rest of the toolkit

This Actor pairs with **[Email List Cleaner & Validator](https://apify.com/nerolabs/email-list-cleaner)**: one cleans the phone column, the other cleans the email column, both keeping every original row.

For the data itself, the Nero Labs dataset tools handle each step of a pipeline: [Cleaner & Exporter](https://apify.com/nerolabs/dataset-cleaner-exporter), [Filter & Transform](https://apify.com/nerolabs/dataset-filter-transform), [Join & Merge](https://apify.com/nerolabs/dataset-join-merge), [Aggregate, Group By & Pivot](https://apify.com/nerolabs/dataset-aggregate-pivot), [Diff & Change Detector](https://apify.com/nerolabs/dataset-diff-detector), [AI Enrich](https://apify.com/nerolabs/dataset-ai-enrich), [Charts & Report](https://apify.com/nerolabs/dataset-charts-report), [to Postgres, Supabase & MySQL](https://apify.com/nerolabs/dataset-to-database), [to REST API](https://apify.com/nerolabs/dataset-to-rest-api), and [Actor Pipeline Runner](https://apify.com/nerolabs/actor-pipeline-runner) to chain them in one call.

***

If this saved you a batch of failed texts or an afternoon of matching results back to spreadsheet rows, a review on the Store page helps a lot. If something looks wrong, open an issue on the Issues tab and I will answer personally.

# Actor input Schema

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

Pick an existing Apify dataset, for example the output of any scraper run. Use this OR 'File URL' OR 'Data (inline)'. Declaring it this way is what lets this Actor run with limited permissions: it may read the dataset you point at, and nothing else on your account.

## `fileUrl` (type: `string`):

A public link to a CSV or TSV file, an Excel .xlsx file, a JSON array or JSON Lines file, or a Google Sheet (paste the normal sheet link with sharing set to 'Anyone with the link can view'). Format is detected automatically. Up to 100 MB.

## `data` (type: `array`):

Paste rows directly instead, for a quick one-off check.

## `fileFormat` (type: `string`):

Only needed if automatic detection gets the file URL's format wrong.

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

Which column holds the number, for example 'phone' or 'contact\_number'. Leave empty and it is detected automatically, first by column name and then by looking at which column actually contains real numbers.

## `defaultRegion` (type: `string`):

The country to assume for numbers stored without a country code, such as '020 7946 0123' or '(212) 555-0123'. Use GB, US, AU, DE and so on. Numbers that already start with + are unaffected. Without this, a number with no country code cannot be validated at all and is reported as 'unknown\_region' rather than guessed at.

## `flagPremiumRate` (type: `boolean`):

Marks expensive premium-rate lines (UK 09, US 900 and their equivalents) as risky rather than valid. These are real numbers, but calling one costs money and it is almost never a person's own phone.

## `flagVoip` (type: `boolean`):

Marks internet-telephony ranges as risky. VoIP numbers are cheap and quick to create, so on a signup list they play much the same role a throwaway email address does.

## `flagNonPersonal` (type: `boolean`):

Marks toll-free, shared-cost, UAN, pager and voicemail numbers as risky. They work, but they reach a switchboard or a service rather than the contact you have in the row.

## `markDuplicates` (type: `boolean`):

The first time a number appears it is checked normally; later repeats are marked 'duplicate'. Matching is done on the E.164 form, so '020 7946 0123', '+44 20 7946 0123' and '+442079460123' are correctly recognised as the same number.

## `lookupRegion` (type: `boolean`):

Adds a place description where the numbering plan carries one, for example 'London' or 'New York, NY'. Mobile numbers usually have no geographic meaning, so this is normally empty for them.

## `lookupCarrier` (type: `boolean`):

Adds the network the number's range was originally ALLOCATED to, for example Vodafone or Optus. This is not the current network if the number has since been ported, and it needs no live lookup. Available for mobile ranges in most countries, but not in the United States or Canada.

## `lookupTimezone` (type: `boolean`):

Adds the IANA timezone or timezones the number sits in, so a dialling list can respect local calling hours.

## `keep` (type: `string`):

Filters the output. Every row is still checked and charged either way, so this is about what you get back, not about cost.

## `exportFormats` (type: `array`):

Optionally write the results as a real downloadable CSV and/or Excel file, on top of the dataset.

## `outputDatasetName` (type: `string`):

Optional. Every run's results are also appended to a dataset of this name, so a scheduled job builds one growing clean list. Not charged again.

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

Stop after this many rows, a cost guard while you are testing. There is a hard ceiling of 200,000 per run regardless.

## `webhookUrl` (type: `string`):

Optional. If set, the run summary (counts and download links, never the numbers themselves) is POSTed here as JSON when the run finishes, so a scheduled clean-up can report into Slack, Zapier, Make, n8n or your own API. Only charged on a confirmed 2xx.

## Actor input object example

```json
{
  "data": [
    {
      "name": "Ana Silva",
      "company": "Silva Ltd",
      "phone": "+44 7825 123456"
    },
    {
      "name": "Ben Okafor",
      "company": "Okafor & Co",
      "phone": "0161 496 0123"
    },
    {
      "name": "Cara Lind",
      "company": "Lind Studio",
      "phone": "+1 212 555 0123"
    },
    {
      "name": "Dan Reeves",
      "company": "Reeves Ltd",
      "phone": "+44 900 123 4567"
    },
    {
      "name": "Eve Mbeki",
      "company": "Mbeki Group",
      "phone": "+61 412 345 678"
    },
    {
      "name": "Frank Ortiz",
      "company": "Ortiz SA",
      "phone": "call the office"
    },
    {
      "name": "Gina Haas",
      "company": "Haas Studio",
      "phone": "+44 56 0000 0000"
    },
    {
      "name": "Ana Silva (again)",
      "company": "Silva Ltd",
      "phone": "07825 123456"
    }
  ],
  "fileFormat": "auto",
  "defaultRegion": "GB",
  "flagPremiumRate": true,
  "flagVoip": true,
  "flagNonPersonal": true,
  "markDuplicates": true,
  "lookupRegion": true,
  "lookupCarrier": true,
  "lookupTimezone": true,
  "keep": "all",
  "exportFormats": []
}
```

# Actor output Schema

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

Every original row with its verdict, line type, country, region, carrier and E.164 format added.

## `checkSummary` (type: `string`):

Counts by verdict, reason, line type and country, plus an explicit note on what was and was not checked.

# 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 = {
    "data": [
        {
            "name": "Ana Silva",
            "company": "Silva Ltd",
            "phone": "+44 7825 123456"
        },
        {
            "name": "Ben Okafor",
            "company": "Okafor & Co",
            "phone": "0161 496 0123"
        },
        {
            "name": "Cara Lind",
            "company": "Lind Studio",
            "phone": "+1 212 555 0123"
        },
        {
            "name": "Dan Reeves",
            "company": "Reeves Ltd",
            "phone": "+44 900 123 4567"
        },
        {
            "name": "Eve Mbeki",
            "company": "Mbeki Group",
            "phone": "+61 412 345 678"
        },
        {
            "name": "Frank Ortiz",
            "company": "Ortiz SA",
            "phone": "call the office"
        },
        {
            "name": "Gina Haas",
            "company": "Haas Studio",
            "phone": "+44 56 0000 0000"
        },
        {
            "name": "Ana Silva (again)",
            "company": "Silva Ltd",
            "phone": "07825 123456"
        }
    ],
    "defaultRegion": "GB"
};

// Run the Actor and wait for it to finish
const run = await client.actor("nerolabs/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 = {
    "data": [
        {
            "name": "Ana Silva",
            "company": "Silva Ltd",
            "phone": "+44 7825 123456",
        },
        {
            "name": "Ben Okafor",
            "company": "Okafor & Co",
            "phone": "0161 496 0123",
        },
        {
            "name": "Cara Lind",
            "company": "Lind Studio",
            "phone": "+1 212 555 0123",
        },
        {
            "name": "Dan Reeves",
            "company": "Reeves Ltd",
            "phone": "+44 900 123 4567",
        },
        {
            "name": "Eve Mbeki",
            "company": "Mbeki Group",
            "phone": "+61 412 345 678",
        },
        {
            "name": "Frank Ortiz",
            "company": "Ortiz SA",
            "phone": "call the office",
        },
        {
            "name": "Gina Haas",
            "company": "Haas Studio",
            "phone": "+44 56 0000 0000",
        },
        {
            "name": "Ana Silva (again)",
            "company": "Silva Ltd",
            "phone": "07825 123456",
        },
    ],
    "defaultRegion": "GB",
}

# Run the Actor and wait for it to finish
run = client.actor("nerolabs/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 '{
  "data": [
    {
      "name": "Ana Silva",
      "company": "Silva Ltd",
      "phone": "+44 7825 123456"
    },
    {
      "name": "Ben Okafor",
      "company": "Okafor & Co",
      "phone": "0161 496 0123"
    },
    {
      "name": "Cara Lind",
      "company": "Lind Studio",
      "phone": "+1 212 555 0123"
    },
    {
      "name": "Dan Reeves",
      "company": "Reeves Ltd",
      "phone": "+44 900 123 4567"
    },
    {
      "name": "Eve Mbeki",
      "company": "Mbeki Group",
      "phone": "+61 412 345 678"
    },
    {
      "name": "Frank Ortiz",
      "company": "Ortiz SA",
      "phone": "call the office"
    },
    {
      "name": "Gina Haas",
      "company": "Haas Studio",
      "phone": "+44 56 0000 0000"
    },
    {
      "name": "Ana Silva (again)",
      "company": "Silva Ltd",
      "phone": "07825 123456"
    }
  ],
  "defaultRegion": "GB"
}' |
apify call nerolabs/phone-number-validator --silent --output-dataset

```

## MCP server setup

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