# Phone Number Validator - Bulk Validate, Format & Clean (`stas_k/phone-number-validator`) Actor

Built after the complaints under phone validators: runs that only reveal the actor's limits, numbers you never sent, one at a time. Here: paste 10,000 lines, every line back exactly as sent, pay only for unique valid numbers - junk and duplicates free. Offline libphonenumber, reason per line.

- **URL**: https://apify.com/stas_k/phone-number-validator.md
- **Developed by:** [Stas K](https://apify.com/stas_k) (community)
- **Categories:** Developer tools, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$0.80 / 1,000 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.

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 Validate, Format & Clean

Validate, format and clean phone numbers from any country with Google's libphonenumber, the library behind Android
and billions of contact records. Up to 10,000 lines per run. Every line comes back exactly as you sent it, with a
plain reason when it fails.

### What this Actor does not do

Stated up front, so nobody pays a run to find out:

- **No live carrier lookup.** The `carrier` field is allocation-time metadata from the bundled libphonenumber
  dataset. It can be empty for many countries. Empty means unknown, not "no carrier".
- **No reverse lookup.** This is not an owner, name, email or address finder.
- **No WhatsApp or Telegram registration check.** The actor never contacts WhatsApp, Telegram or any phone.
- **No guessing.** A local number written without a country code is a reason in the row, not a silent guess.
  Set `Default country` if your list is written that way.

### Three problems this Actor was built to fix

Taken from public reviews of other phone actors in this Store:

1. **Sample data instead of your data.** One actor returned "some another phone numbers" and the answer was
   "that is the sample data". Here an empty input fails the run with a message. There is no demo row anywhere.
2. **No bulk.** One actor accepted a single number per call. Here a list or a pasted column is the normal input,
   order preserved, up to 10,000 lines.
3. **Paying for junk.** Here you are charged only when a unique valid number is delivered. Invalid lines,
   unparsed lines and duplicate lines cost nothing, and each of them still appears in the output with its reason.

### Input

```json
{
  "numbers": [
    "+1 (202) 555-0173 ext. 42",
    "tel:+44 20 7946 0018",
    "+34 (0) 91 123 45 67",
    "044 123 45 67",
    "not a phone"
  ],
  "defaultCountry": "UA"
}
```

`numbers` is a list, `text` is a pasted block used only when the list is empty. Messy lines are accepted:
`tel:` links, labels such as Phone, Tel., Call, Mobile, Mob, `(0)` trunk prefixes after the country code in any
country (for example `+34 (0) 91 ...`, `+7 (0) 495 ...`, `+1 (0) 202 ...`), `00` international prefixes,
spaces, dashes, dots, slashes, parentheses, non-breaking spaces, full-width digits, Arabic-Indic and Persian
digits, and extensions written as `ext`, `extension`, `x`, `#`, `dob`.

**One warning about `Default country`.** It is meant for lists written in the local format, such as `044 123 45 67`
for Ukraine. If you set it on a list of dates, order numbers or other digit strings, some of them can match a real
number format of that country and come back `VALID`. With no default country, a line without a country code is a
reason, not a guess, and none of the 30 noise samples we test (dates, timestamps, IBAN, VAT, coordinates, prices)
becomes valid.

### Output

One row per input line:

| Field | Meaning |
|---|---|
| `index` | Line number, starting at 1, in the order you sent it. A blank line inside the list keeps its row, so indexes still match your source list. |
| `inputAsGiven` | The line exactly as received, so rows join back to your list |
| `status` | `VALID`, `POSSIBLE_ONLY`, `INVALID`, `NO_NUMBER`, `ERROR`, `NOT_CHECKED_CAP`, `NOT_CHECKED_BILLING`, `NOT_CHECKED_LIMIT` |
| `reason` | Plain-language explanation for every non-valid line |
| `valid`, `possible` | libphonenumber verdicts |
| `type` | `mobile`, `fixed_line`, `fixed_line_or_mobile`, `voip`, `toll_free`, `premium_rate`, `shared_cost`, `uan`, `pager`, `voicemail`, `personal_number`, `unknown` |
| `countryCode`, `countryName`, `callingCode` | ISO-3166 alpha-2, country name, `+380` |
| `e164`, `international`, `national`, `rfc3966` | All four formats |
| `extension` | Parsed extension, or null |
| `smsCapable` | true only for valid mobile and fixed-line-or-mobile numbers |
| `timeZones` | Time zones of the number's region, useful for calling hours |
| `regionDescription` | City or area, when the metadata knows it |
| `carrier` | Allocation-time carrier from the offline dataset, or empty |
| `otherNumbers` | Additional valid numbers found in the same line |
| `duplicateOfIndex` | Index of the first occurrence, when this number already appeared |
| `billable`, `chargedEvents`, `chargedNumbers` | What this row cost and which numbers were charged |
| `chargeUncertain` | true when billing failed but the row was still delivered |
| `billingInactive` | true when the run could not charge at all (not pay-per-event); the row was delivered free |

### Billing

`number-validated` costs **$0.80 per 1,000 unique valid numbers** (`$0.0008` each).

- Charged once per unique valid number delivered.
- **Each unique valid number found in a line is its own event.** A line saying `Call +1 202 555 0173 or
  +44 20 7946 0018` contains two numbers and costs two events, and both are listed in `otherNumbers` and
  `chargedNumbers`, so the dataset always shows what was charged.
- Invalid, unparsed, blank and duplicate lines: free.
- Example: 1,000 lines, 700 valid, 60 duplicates of earlier lines, 240 invalid.
  700 - 60 = 640 charged numbers = **$0.51**. The same list at a per-line price of $0.60 per 1,000 would cost
  $0.60 and return nothing for the 240 rejects.

If a run hits the spending cap set on it (`maxTotalChargeUsd`), the actor stops charging and the remaining lines
are delivered as `NOT_CHECKED_CAP` rows with a reason. If billing itself fails twice in a row, the run stops the
same way but marks the rest `NOT_CHECKED_BILLING`, so a billing problem is never reported as your cap.
No line is ever dropped silently.

If the run is not pay-per-event at all (for example the first run right after a new version, before a price is
enabled for it), no event can be charged: every row is still delivered, `billingInactive` is true on them,
`billingActive` is false in the summary and `stopReason` is `billing-not-active`. You never get a false
"spending cap" for that.

### Limits

- 10,000 lines per run.
- Lines beyond the limit are reported as `NOT_CHECKED_LIMIT` rows (up to 5,000 of them) and never charged.
  The summary shows `truncatedLines` when the list was longer.

### Run summary

`RUN_SUMMARY` is written to the named key-value store `phone-validator-summary` and also to the run's default store.
It holds counts by status, unique valid numbers, charged events, duplicates, `stopReason`, `billingActive`,
`truncatedLines` and `chargeErrors`.

### Accuracy

Validation is offline and follows the bundled libphonenumber metadata. `VALID` means the number matches an assigned
range for its country in this build. Ranges assigned after the dataset was built can come back as `INVALID`; the
reason says the prefix is not in range. Nothing here proves that a number currently answers, and no number is ever
dialled.

# Actor input Schema

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

One number per line. Messy lines are fine: tel: links, labels such as Phone or Tel, (0) trunk prefixes, 00 international prefixes, spaces, dashes, dots, slashes, full-width and Arabic-Indic digits, extensions (ext, x, #, dob). Up to 10,000 lines per run.

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

Used only when 'Numbers to validate' is empty. One number per line; a pasted column from a spreadsheet works too.

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

Two letters such as UA, US, GB, DE. Needed only for numbers written without a country code (for example 044 123 45 67). Empty means auto: only numbers with a country code are read, the rest come back with a reason instead of a guess.

## Actor input object example

```json
{
  "numbers": [
    "+1 (202) 555-0173 ext. 42",
    "tel:+44 20 7946 0018",
    "+380 44 123 45 67",
    "call me maybe"
  ]
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per input line: the line as given, status, the reason for a reject, E.164, international, national and RFC 3966 formats, type, country, SMS capability, time zones, duplicate marker and whether the row was charged.

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

Counts by status, unique valid numbers, charged events, duplicates, stopReason (spending cap, billing problem or billing not active), billingActive and truncation. Stored in the named key-value store phone-validator-summary as RUN_SUMMARY.

# 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": [
        "+1 (202) 555-0173 ext. 42",
        "tel:+44 20 7946 0018",
        "+380 44 123 45 67",
        "call me maybe"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("stas_k/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 = { "numbers": [
        "+1 (202) 555-0173 ext. 42",
        "tel:+44 20 7946 0018",
        "+380 44 123 45 67",
        "call me maybe",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("stas_k/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 '{
  "numbers": [
    "+1 (202) 555-0173 ext. 42",
    "tel:+44 20 7946 0018",
    "+380 44 123 45 67",
    "call me maybe"
  ]
}' |
apify call stas_k/phone-number-validator --silent --output-dataset

```

## MCP server setup

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