# Bulk Phone Number Validation - E.164, Line Type, $0.50/1,000 (`pennydata/phone-number-validator-offline`) Actor

Bulk phone number validation with libphonenumber: validity, E.164, country, line type, formats. Offline, no API key. First 25 numbers free per run, then $0.50 per 1,000. Accepts phoneNumbers and defaultCountry inputs from other validators.

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

## Pricing

from $0.35 / 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

## Bulk Phone Number Validation - E.164, Line Type & Format Check ($0.50 per 1,000)

**First 25 numbers per run are free; after that $0.50 per 1,000. Offline libphonenumber, no API key.** Enrichment: country, region, timezone, line type.

Validate, parse and format phone numbers in bulk using Google's libphonenumber metadata (via `libphonenumber-js`). Blank or unparseable inputs are returned with an error code.

**Try it free:** turn on `sampleOnly` ("Free sample: first 10 numbers, no charge") to validate the first 10 numbers of your list with no charge at all.

Drop-in compatible with pink_comic/phone-number-validation input (`phoneNumbers`, `defaultRegion`, `maxResults`, `includeCarrier`, `includeGeocoding`, `includeTimezone`).

Inputs saved for other phone validators also run here: `phoneNumbers` (or `numbers`, `phone`) in place of `phones`, and `defaultCountry` / `defaultCountryCode` in place of `defaultRegion`. Fields we do not return (`includeCarrier`, `includeGeocoding`, `includeTimezone`, `formats`, `maxConcurrency`) are accepted and ignored; `maxResults` limits the list. Output per row: E.164, validity, line type, country, calling code, national and international format; with `includeGeoCarrier`, also carrier, region and time zones (offline).

Paste phone numbers in any format, get back E.164, validity, line type and country. 1,000 numbers = $0.50.

| input | e164 | valid | type | country |
|---|---|---|---|---|
| +1 415-555-2671 | +14155552671 | true | FIXED_LINE_OR_MOBILE | US |
| +44 20 7946 0958 | +442079460958 | true | FIXED_LINE | GB |
| (212) 555-0100 | +12125550100 | true | FIXED_LINE_OR_MOBILE | US |
| not a number | null | false | null | null |

Offline format check (libphonenumber): no carrier lookup, no API key. Unparseable rows are free. Rows above are real output of this actor's validator with `defaultRegion` US.

Pairs with [Bulk Email Validator (no SMTP)](https://apify.com/pennydata/bulk-email-validator-no-smtp) for lead-list hygiene.

### Features

- **Per-row status and run summary:** every output row has `rowStatus` (`ok`, `invalid` or `error`) and `rowReason`, and each run saves a `RUN_SUMMARY` record in the key-value store with counts per status, rows billed and free rows.

### Quick start

Input (`phones` is the only field you need):

```json
{ "phones": ["+1 415-555-2671", "+44 20 7946 0958", "abc"], "defaultRegion": "US" }
```

Numbers in a Google Sheet? Set `sheetUrl` to the share link (shared as "Anyone with the link can view"; converted to CSV export automatically) and optionally `sheetColumn`: `{ "sheetUrl": "https://docs.google.com/spreadsheets/d/<id>/edit#gid=0", "sheetColumn": "Phone", "defaultRegion": "US" }`.
Output, one row per number (illustrative shape; values for `+14155552671`):

```json
{ "input": "+1 415-555-2671", "e164": "+14155552671", "valid": true, "possible": true, "type": "FIXED_LINE_OR_MOBILE", "country": "US", "callingCode": "1", "nationalFormat": "(415) 555-2671", "internationalFormat": "+1 415-555-2671", "error": null }
```

Pricing event: `phone-validated`, $0.0005 per parseable number; blank or unparseable inputs are free.

**Input:** `phones` (list of numbers in any common format), `defaultRegion` (ISO 2-letter code, used for numbers without a `+` prefix, default US) and `sampleOnly` (process only the first 10 numbers, no charge). Inputs saved for other validators also work: `phoneNumbers`, `numbers` or `phone` are accepted in place of `phones`; `phoneNumbersText` takes numbers pasted one per line, `defaultCountry` works like `defaultRegion`, and `onlyValid` returns only valid numbers (invalid ones are left out and not charged).

**Output per number:** `input`, `e164`, `valid`, `possible`, `type` (MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE, TOLL_FREE, VOIP, ...; null if unknown), `country`, `callingCode`, `nationalFormat`, `internationalFormat`, `error`.

### Phone number formatter for WhatsApp (E.164 + wa.me link)

Phone number formatter for WhatsApp: E.164 + wa.me click-to-chat link (format only, no WhatsApp account check). The E.164 digits are what a `https://wa.me/<digits>` link expects. This Actor does not contact WhatsApp and cannot tell you whether a number has a WhatsApp account.

### Validate leads from a Google Maps Scraper run

1. Run a Google Maps scraper (for example `compass/crawler-google-places`) and copy the dataset ID of the run (Storage tab).
2. Paste it into `inputDatasetId`. The actor reads `phoneUnformatted`, `internationalPhoneNumber` or `phone` from each place (first one present) and validates it. Set `defaultRegion` to the scrape's country for numbers without a `+`.
3. Each result row keeps the place's `title`, `placeId`, `website` and `address` when present, so you can join results back to the scrape.

Example: `{ "inputDatasetId": "YOUR_DATASET_ID", "defaultRegion": "US", "onlyValid": true }`. This checks format and line type offline; it does not call the number or confirm it is in service.

### UK, India and Brazil phone number validator

UK, India and Brazil phone number validator: +44, +91, +55 numbers parsed, validated and formatted offline. Set `defaultRegion` to `GB`, `IN` or `BR` for local-format numbers, or use the `+` prefix.

### FAQ

#### How do I convert a list of phone numbers to E.164 in bulk?

Put the numbers in `phones` and set `defaultRegion` (for example `US`, `GB`, `DE`) for numbers written without a `+` prefix. Every row comes back with `e164`, `nationalFormat` and `internationalFormat`. Example input for a mixed list (the first has no `+`, so `defaultRegion` is used):

```json
{ "phones": ["(415) 555-2671", "+44 20 7946 0958", "+49 30 123456"], "defaultRegion": "US" }
```

Numbers that can be parsed cost $0.0005 each ($0.50 per 1,000). Blank or unparseable inputs are returned with an `error` code and are not charged.

#### Can I clean a phone list before an SMS campaign?

Partly. Use `valid` and `possible` to find numbers that cannot exist (wrong length, no matching numbering-plan range) and `type` to see line type where the numbering plan says. It does not check whether a number is active, reachable or ported; that needs a paid carrier/HLR lookup. Turn on `sampleOnly` to run the first 10 numbers of your list free before you pay for the rest.

#### Is this number mobile or landline?

The `type` field returns MOBILE, FIXED_LINE, FIXED_LINE_OR_MOBILE, TOLL_FREE, VOIP and other libphonenumber types, or null when unknown. In the US and Canada mobile and landline share ranges, so you will often see FIXED_LINE_OR_MOBILE. This is numbering-plan metadata only, not a carrier check.

#### Does it work offline, with no API key?

Yes. The Actor runs Google's libphonenumber metadata (via `libphonenumber-js`) inside the run: no third-party lookup service, no API key and no per-lookup vendor fee. You pay only the Actor's pay-per-event price of $0.50 per 1,000 parseable numbers.

#### Do I need an API key or external credits?

No. There is no third-party lookup account, API key or prepaid credit balance to top up, so you cannot hit a "no balance" error from an outside service. The only cost is this Actor's pay-per-event charge on your Apify account. Set `sampleOnly` to validate the first 10 numbers free.

#### What does it not return?

No live-line / active / reachable / ported status, no WhatsApp or SMS deliverability, no owner name and no personal-data lookup. It returns only what libphonenumber's numbering-plan metadata can say: format, validity, line type and country, plus carrier (original allocation), region and time zones when `includeGeoCarrier` is on.

#### I already use libphonenumber. Why pay for this?

You don't have to if you are happy to run it in your own code. Use this Actor when you want phone validation as an API or scheduler step (Make, n8n, Zapier, or a direct API call) or as a one-off CSV job without setting up code. Results match libphonenumber output; there is no extra data beyond that.

#### What about emails in the same list?

Use [Bulk Email Validator (No SMTP)](https://apify.com/pennydata/bulk-email-validator-no-smtp) for the email column of the same lead list.

### Use it from Google Sheets (=PHONE_E164)

Paste this Apps Script into Extensions > Apps Script, add your own Apify token as script property `APIFY_TOKEN` (never in the code), then use `=PHONE_E164(A2:A50)` to get E.164, valid, line type and country per row. It calls this Actor's `run-sync-get-dataset-items` endpoint on your account.

```javascript
function PHONE_E164(range, defaultRegion) {
  var token = PropertiesService.getScriptProperties().getProperty('APIFY_TOKEN');
  if (!token) throw new Error('Set script property APIFY_TOKEN.');
  var rows = Array.isArray(range) ? range : [[range]], flat = [];
  rows.forEach(function (r) { r.forEach(function (c) { flat.push(String(c == null ? '' : c).trim()); }); });
  var phones = flat.filter(function (p, i) { return p && flat.indexOf(p) === i; });
  if (!phones.length) return flat.map(function () { return ['', '', '', '']; });
  var res = UrlFetchApp.fetch('https://api.apify.com/v2/acts/pennydata~phone-number-validator-offline/run-sync-get-dataset-items?format=json&clean=true', {
    method: 'post', contentType: 'application/json', headers: { Authorization: 'Bearer ' + token },
    payload: JSON.stringify({ phones: phones, defaultRegion: defaultRegion || 'US' }), muteHttpExceptions: true });
  if (res.getResponseCode() >= 300) throw new Error('Apify HTTP ' + res.getResponseCode());
  var by = {}; JSON.parse(res.getContentText()).forEach(function (it) { by[it.input] = it; });
  return flat.map(function (p) { var it = by[p]; return !p ? ['', '', '', ''] : !it ? ['no result', '', '', ''] : [it.e164 || '', it.valid === true, it.type || '', it.country || '']; });
}
```

Runs are billed to your Apify account at the per-number price below.

### Pricing

Pay per event, no start fee. Prices match `.actor/pay_per_event.json`.

| Event | Price | When it is charged |
|---|---|---|
| `phone-validated` | $0.0005 per number ($0.50 per 1,000) | One number that could be parsed and returned (valid or invalid) |
| Blank or unparseable input | Free | Returned with an `error` code, never charged |

Example: 1,000 inputs of which 40 are blank or unparseable cost 960 x $0.0005 = $0.48. Set a maximum cost per run in the run options; the actor stops charging when that limit is reached. Apify platform usage is billed separately if your plan includes it.

### Honest limits

- **Offline only. No carrier lookup, no HLR, no "is this number active/reachable/ported" check, no owner or personal-data lookup.** `valid` means the number matches a real numbering plan range; it may not be assigned to anyone.
- Line type comes from libphonenumber metadata and is not always determinable (US numbers are often `FIXED_LINE_OR_MOBILE`).
- Time zones come from libphonenumber metadata (opt-in via `includeGeoCarrier`); where an area spans several zones all are returned.
- Numbers without a `+` prefix use `defaultRegion`; a wrong region gives wrong results.
- Metadata is as current as the bundled library version.

Not affiliated with Google or the libphonenumber project. Written with the assistance of an AI (Claude Sonnet 5.5).

### Switching from another phone validator

Paste the input you already use and change only the actor ID. These keys are accepted as aliases:

| Other validator's input | Used here as |
|---|---|
| `phoneNumbers`, `numbers`, `phone`, `phoneNumbersText` (list, text or `{phone}` objects) | `phones` |
| `defaultCountry`, `defaultCountryCode`, `country` | `defaultRegion` |
| `maxResults` | cap on numbers processed |
| `includeCarrier`, `includeGeocoding`, `includeTimezone`, `formats`, `maxConcurrency` | accepted and ignored (use `includeGeoCarrier` for offline carrier, region and time zone) |

Input is compatible; the output field names are this actor's own (`e164`, `valid`, `type`, `country`), so check your downstream mapping once.

### Use from Claude, Cursor or any MCP client

Apify's MCP server can expose this actor as a tool. Use this URL (format from the [Apify MCP docs](https://docs.apify.com/platform/integrations/mcp), `?tools=` takes a comma-separated list of actors):

`https://mcp.apify.com/?tools=pennydata/phone-number-validator-offline`

Client config (`mcpServers` format; sign in with OAuth when the client asks):

```json
{"mcpServers": {"apify": {
  "url": "https://mcp.apify.com/?tools=pennydata/phone-number-validator-offline"
}}}
```

Example prompt: "Validate these phone numbers with default region GB and give me E.164 and line type: 020 7946 0958, 07911 123456."

### Related actors

Other small pay-per-event Actors from the same publisher:

- [Bulk Email Validator (No SMTP)](https://apify.com/pennydata/bulk-email-validator-no-smtp): validate the email column of the same lead list.
- [Website Contact Extractor](https://apify.com/pennydata/website-contact-extractor): pulls phone numbers from websites so you can validate them here.
- [Shopify Store Leads & Contacts](https://apify.com/pennydata/shopify-store-leads-contacts): store contact data to run through the validator.

### Geo and carrier (optional)

Set `includeGeoCarrier` to add `carrier`, `geoLocation` (city/region, e.g. US area-code city and state) and `timeZones` (IANA names) to valid numbers. Offline lookup from libphonenumber metadata: carrier is the original allocation and ignores number porting; many fixed-line numbers have no carrier. Off by default.

# Actor input Schema

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

List of phone numbers to validate, in any common format. Numbers without a '+' prefix use the default region. Optional: if you leave it at the sample list and set phoneNumbers, numbers, phone or phoneNumbersText instead, that field is used.

## `inputDatasetId` (type: `string`):

Validate phones from another run's dataset (ID or name), e.g. a Google Maps Scraper (compass/crawler-google-places) run. Fields phoneUnformatted, internationalPhoneNumber and phone are detected automatically and title, placeId, website, address are copied onto each result row. When set, the sample list is ignored.

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

Only needed if your dataset uses another column name. Leave empty to auto-detect phoneUnformatted, internationalPhoneNumber, phone.

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

Two-letter country code (e.g. US, GB, DE) used to interpret numbers written without an international '+' prefix. Leave empty to require the '+' prefix on every number.

## `phoneNumbersText` (type: `string`):

Alternative to the list above: paste numbers separated by new lines, commas or semicolons. Used when set; takes precedence over the sample list.

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

Same as Default region; accepted so inputs saved for other validators work. Default region wins when changed from US.

## `onlyValid` (type: `boolean`):

When on, invalid and unparseable numbers are left out of the output and not charged. Only numbers that pass validation are returned.

## `sampleOnly` (type: `boolean`):

When on, only the first 10 numbers are processed and no pay-per-event charge is made. Use it to try the output format at no cost.

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

Same as Phone numbers; accepted so an input saved for another phone validator runs unchanged. Strings or {phone|number} objects.

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

Same as Phone numbers (alias).

## `country` (type: `string`):

Same as Default region; accepted for inputs saved for other validators.

## `defaultCountryCode` (type: `string`):

Same as Default region; accepted for inputs saved for other validators.

## `maxResults` (type: `integer`):

Process at most this many numbers from the list. 0 or empty means all.

## `includeCarrier` (type: `boolean`):

Accepted for compatibility with other validators; this actor is offline and does not return this field, so the flag is ignored.

## `includeGeocoding` (type: `boolean`):

Accepted for compatibility with other validators; this actor is offline and does not return this field, so the flag is ignored.

## `includeTimezone` (type: `boolean`):

Accepted for compatibility with other validators; this actor is offline and does not return this field, so the flag is ignored.

## `formats` (type: `array`):

Accepted for compatibility; every row always includes E.164, national and international formats.

## `maxConcurrency` (type: `integer`):

Accepted for compatibility; ignored.

## `includeGeoCarrier` (type: `boolean`):

When on, valid numbers get a carrier name (original mobile carrier, from libphonenumber metadata) and a geoLocation text (city or region) and timeZones (IANA names). Offline lookup, so carrier reflects the original allocation and ignores number porting. Off by default; output is unchanged when off.

## `sheetUrl` (type: `string`):

Link to a Google Sheet shared as 'Anyone with the link can view' (docs.google.com/spreadsheets/d/<id>/edit#gid=0) or a CSV URL. Numbers from the phone column are added to Phone numbers.

## `sheetColumn` (type: `string`):

Header of the column with phone numbers. Default: first header containing phone, mobile or tel, else the first column.

## Actor input object example

```json
{
  "phones": [
    "+1 415-555-2671",
    "+44 20 7946 0958",
    "+91 98765 43210",
    "(212) 555-0100",
    "not a number"
  ],
  "defaultRegion": "US",
  "onlyValid": false,
  "sampleOnly": false,
  "includeGeoCarrier": false
}
```

# Actor output Schema

## `runSummary` (type: `string`):

No description

## `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 = {
    "phones": [
        "+1 415-555-2671",
        "+44 20 7946 0958",
        "+91 98765 43210",
        "(212) 555-0100",
        "not a number"
    ],
    "defaultRegion": "US"
};

// Run the Actor and wait for it to finish
const run = await client.actor("pennydata/phone-number-validator-offline").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 415-555-2671",
        "+44 20 7946 0958",
        "+91 98765 43210",
        "(212) 555-0100",
        "not a number",
    ],
    "defaultRegion": "US",
}

# Run the Actor and wait for it to finish
run = client.actor("pennydata/phone-number-validator-offline").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 415-555-2671",
    "+44 20 7946 0958",
    "+91 98765 43210",
    "(212) 555-0100",
    "not a number"
  ],
  "defaultRegion": "US"
}' |
apify call pennydata/phone-number-validator-offline --silent --output-dataset

```

## MCP server setup

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

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/Q2ELhWJDRGnvJUwXa/builds/qg3bhM0whdnRFxXF5/openapi.json
