# Bulk Phone Number Validator & Cleaner — E.164, $0.50/1k (`yabanana99/phone-number-cleaner-validator`) Actor

Clean a phone list end to end: parse and validate with libphonenumber, format to E.164, international and national, detect line type, region, carrier and timezone offline, deduplicate on E.164 and export segmented CSV. Reads numbers buried in free text. No external API calls.

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

## Pricing

from $0.50 / 1,000 phone 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/platform/actors/running/actors-in-store#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

**$0.50 per 1,000 numbers, the same price on every plan, and duplicates are free.**
Everything runs inside the Actor container with Google's **libphonenumber** metadata: no HLR lookup, no third-party API, **no number ever leaves the platform**. That is also why it is this cheap — there is no per-lookup cost to pass on to you.

Upload the CSV your CRM exported and get back every number **parsed, validated, deduplicated on E.164 and formatted**, with line type, region, carrier and time zones — plus `valid.csv`, `possible.csv`, `invalid.csv` and `mobile.csv` ready to import, and a report that tells you what was actually wrong with your list.

### Why this Actor

- **Deduplication that understands phone numbers.** `+39 333 1234567`, `0039 333 1234567`, `(333) 123-4567` and `333 1234567` are **one contact**, not four rows. A spreadsheet's "Remove duplicates" compares strings; this compares E.164. Extensions are kept apart, so two desks behind one switchboard stay two contacts.
- **Files in, files out.** CSV, TSV, XLSX, JSON, JSON Lines and plain-text lists — up to 20 files merged and deduplicated *together*. The phone column is auto-detected **and always reported**, wide CRM exports are read to the end, and `include_source_row` gives you back every other column, so the cleaned list is still a contact list.
- **It finds numbers buried in text.** `call me on 333 1234567 after 6pm`, `+39 333 1234567 (home)`: a notes column is not a dead end. Validators that only accept one clean number per row return "invalid" for every one of those rows — and bill you for them.
- **It tells you when your run is pointless *before* charging you.** A national list with no `default_region` set, a column of order ids, a name column: the run stops with a diagnosis (including *which* region would have validated your numbers) and you pay for the sample, not for the file.
- **Segments you can act on.** `valid` / `possible` / `invalid`, exported as ready-to-import CSV files, plus a cross-cutting `mobile.csv` — the numbers an SMS or WhatsApp campaign can actually use.
- **Honest about what validation can prove.** See [What "valid" means](#what-valid-means-and-what-it-does-not).

### Input

Provide inline numbers, files, or both — they are merged and deduplicated together.

```json
{
    "phones": ["+39 333 1234567", "0039 333 1234567", "(212) 555-0123"],
    "sources": [
        { "url": "https://example.com/exports/contacts.csv" },
        { "key": "hubspot-export.xlsx", "store_id": "abcDEF123" },
        { "base64": "bmFtZSxwaG9uZQo...", "filename": "list.csv" }
    ],
    "default_region": "IT",
    "phone_column": "",
    "dedup": true,
    "output_formats": ["e164", "international", "national", "rfc3966"],
    "strict": false,
    "detect_type": true,
    "carrier_lookup": true,
    "timezone_lookup": true,
    "geo_lookup": true,
    "extract_from_text": true,
    "output_segments": true,
    "include_source_row": false
}
```

| Option | Values | Default | What it does |
| --- | --- | --- | --- |
| `phones` | array of strings | – | Inline numbers |
| `sources` | array of `{url}` / `{base64}` / `{key,store_id}` | – | Up to **20 files**, CSV/TSV/XLSX/JSON/JSONL/TXT |
| `default_region` | ISO-3166-1 alpha-2 (`US`, `GB`, `IT`…) | – | Country assumed for numbers written without a country code |
| `phone_column` | string | auto | Header name of the phone column; empty = auto-detect |
| `dedup` | boolean | `true` | `true`: one record per unique number (matched on E.164). `false`: one record per input row, duplicates flagged |
| `output_formats` | `e164`, `international`, `national`, `rfc3966` | all four | Which formats to write. E.164 is always produced (it is the dedup key) |
| `strict` | boolean | `false` | Send possible-but-not-valid numbers to `invalid` instead of `possible` |
| `detect_type` | boolean | `true` | Mobile / fixed line / VoIP / toll-free / premium rate / … |
| `carrier_lookup` | boolean | `true` | Carrier from the bundled offline metadata (mobile ranges only) |
| `timezone_lookup` | boolean | `true` | IANA time zones for the number |
| `geo_lookup` | boolean | `true` | Area description (`Milan`, `New York, NY`) for geographic numbers |
| `extract_from_text` | boolean | `true` | Read the number out of a cell that also contains text |
| `output_segments` | boolean | `true` | Write `valid.csv`, `possible.csv`, `invalid.csv`, `mobile.csv` to the run's key-value store |
| `include_source_row` | boolean | `false` | Copy the original row into `source_row` (max 30 columns, 500 chars per value, 1000 chars per row) |
| `ignore_low_validity` | boolean | `false` | Process the whole file even when the first 1000 numbers contain nothing valid |
| `max_numbers` | 1–200000 | `200000` | Safety cap on rows processed *and charged* |
| `max_file_size_mb` | 1–100 | `50` | Bigger files are skipped with a warning |

#### `default_region`: the one setting that matters

A number written `+39 333 1234567` carries its country; a number written `333 1234567` does not, and **no validator on earth can check it without knowing the country**. So:

- numbers starting with `+` or `00` are validated whatever you set;
- for the others, `default_region` is the country assumed (two letters: `US`, `GB`, `DE`, `IT`, `IN`, `BR`, …);
- if your list is national and you leave `default_region` empty, the run **stops before validating anything**, tells you which region would have validated it (checked against all 245 regions libphonenumber knows) and charges you only the run start. It never applies that guess for you — stamping 200,000 numbers with a country nobody confirmed is exactly the kind of confident mistake that makes a validated list worthless.

#### How the phone column is chosen (and how you know)

1. `phone_column`, if you set it.
2. Otherwise an unambiguous header name (`phone`, `mobile`, `telephone`, `cell`, `msisdn`, `telefono`, `cellulare`, `téléphone`, `telefonnummer`, `numer telefonu`…). Ambiguous ones (`number`, `contact`, `fax`, `line`) are only considered after those.
3. **Whatever the name says, the data has the last word.** The chosen column is sampled and actually parsed with libphonenumber: if fewer than 30% of its values are valid numbers *and* fewer than 30% are even phone-shaped, the column whose values really are numbers is used instead — a `Phone,Company,Mobile` file where `Phone` holds ticket ids is read from `Mobile`. If no column qualifies, **nothing is read and nothing is charged**.
4. The result is always in the log and in the run report: `Phone column: 'Mobile' (index 2), chosen by content (most values are valid phone numbers); 100% of the 200 sampled value(s) are valid phone numbers and 100% look like one.`
5. A cell that is not a number but **contains** one has the number extracted, so a `notes` column yields contacts instead of 1,200 invalid rows.
6. Auto-detection is strict on purpose, but **`phone_column` is an instruction**: a column you name is read even when only a few of its values look like numbers (the rest come back as invalid), as long as it holds at least one.

Malformed files are recovered where possible: a single unbalanced `"` (the classic way a CSV loses 99% of its rows in silence) is caught by comparing the numbers extracted with the numbers visible in the raw text, then re-parsed ignoring quotes — and the alternative parse is kept **only if it yields more real numbers**, never because it yields more rows. A valid file with multi-line quoted fields (notes, postal addresses — what Excel, Sheets and HubSpot export every day) is therefore never shredded.

### Output

One dataset record per number (or per row, with `dedup: false`):

```json
{
    "phone_original": "call me on 333 1234567 after 6pm",
    "e164": "+393331234567",
    "international": "+39 333 123 4567",
    "national": "333 123 4567",
    "rfc3966": "tel:+39-333-123-4567",
    "is_valid": true,
    "is_possible": true,
    "validation_reason": "is_possible",
    "region_code": "IT",
    "country_code": 39,
    "national_number": "3331234567",
    "extension": null,
    "number_type": "mobile",
    "location": null,
    "carrier": "TIM",
    "carrier_may_have_changed": true,
    "timezones": ["Europe/Rome", "Europe/Vatican"],
    "is_duplicate": false,
    "duplicate_of": null,
    "occurrences": 3,
    "score": 100,
    "segment": "valid",
    "reasons": ["note:number_extracted_from_surrounding_text"],
    "source": "contacts.csv",
    "row_number": 42,
    "source_row": null,
    "summary_json": null,
    "warnings": []
}
```

`phone_original` is always the cell as it was in your file — including the surrounding text when the number was extracted from it. `reasons` is the audit trail: scoring rules (`possible_but_not_a_valid_number`, `premium_rate_number`, `not_a_possible_number:too_short`) plus every cleaning step applied, prefixed with `note:` (`note:number_extracted_from_surrounding_text`, `note:label_prefix_stripped`, `note:additional_numbers_in_cell_ignored:1`). Nothing is changed silently.

`number_type` is one of `mobile`, `fixed_line`, `fixed_line_or_mobile`, `toll_free`, `premium_rate`, `shared_cost`, `voip`, `personal_number`, `pager`, `uan`, `voicemail`, `unknown`. `unknown` means the metadata cannot tell the ranges apart in that country — it is not a defect of the number.

**Files written to the run's key-value store:**

| Key | Content |
| --- | --- |
| `valid.csv` / `possible.csv` / `invalid.csv` | The three segments, ready to import |
| `mobile.csv` | Cross-cutting: every **valid** number whose type is `mobile` or `fixed_line_or_mobile` — the SMS-reachable list |
| `PHONE_REPORT` | The full report as JSON |

Segments larger than one record are split into `valid-2.csv`, `valid-3.csv`, … — never silently truncated. Key-value-store files are used rather than named datasets so a daily scheduled run does not slowly fill your account with storages.

**The last dataset record is the report** (`phone_original: "_summary"`), carrying the same columns as every other record plus `summary_json` — a JSON string, so exporting the dataset to CSV gains one column, not one per report field:

```json
{
    "input_rows": 12480, "unique_numbers": 10233, "duplicates_found": 2247,
    "duplicate_rate_pct": 18.0, "stopped_reason": null,
    "charged_numbers": 10180, "free_records": 53,
    "estimated_cost_usd_at_list_price": 5.095,
    "segments": { "valid": 8410, "possible": 1102, "invalid": 721 },
    "by_number_type": { "mobile": 6120, "fixed_line": 2100, "toll_free": 90 },
    "by_region": [{ "region_code": "IT", "count": 7400 }],
    "by_country_code": [{ "country_code": "39", "count": 7400 }],
    "top_carriers": [{ "carrier": "TIM", "count": 2210 }],
    "not_valid_reasons": { "possible_but_not_valid": 1102, "too_short": 410, "not_a_phone_number": 311 },
    "mobile_numbers": 6120, "numbers_with_extension": 24, "extracted_from_text": 87,
    "suggested_default_region": null,
    "segment_files": { "valid.csv": 8410, "mobile.csv": 6120 },
    "options": { "...": "the options this run actually used" },
    "warnings": ["..."], "seconds": 9.4
}
```

### Scoring and segments

Deterministic and auditable — you can recompute any record by hand from this table.

| Situation | Score | Segment |
| --- | --- | --- |
| Valid number (right length **and** an assigned range in that country) | 100 | `valid` |
| Possible but not valid (plausible length, no assigned range) | 60 | `possible` (`invalid` with `strict`) |
| Possible only as a local number (area code missing) | 55 | `possible` (`invalid` with `strict`) |
| Not possible: too short, too long, invalid length, unknown country code | 0 | `invalid` |
| No phone number could be read from the value at all | 0 | `invalid` (and **free**) |

Valid numbers whose type is worth knowing before you dial keep their score and get a reason instead: `premium_rate_number`, `toll_free_number`, `shared_cost_number`, `voip_number`, `pager_number`, `voicemail_number`, `personal_number`, `universal_access_number`.

### What "valid" means (and what it does not)

libphonenumber checks a number against Google's metadata of national numbering plans. A `valid` verdict means:

> **this number is correctly formed and falls inside a range that country has assigned to an operator.**

It does **not** mean the number is in service, that a human answers it, or that it belongs to the person in the row. Nothing offline can tell you that: only a network (HLR) lookup or an actual call can, and both cost real money per number — around 100× what this Actor charges. What this Actor removes, before you pay for anything like that, is the 20-40% of a typical list that is junk: broken numbers, wrong lengths, unassigned ranges, duplicates written four different ways, and cells that were never phone numbers.

Two more limits worth stating plainly:

- **Carrier data is allocation data.** It is the operator the *range* was assigned to. In every country with mobile number portability the subscriber may have moved: the record says so with `carrier_may_have_changed: true`. Carrier metadata only covers mobile (and pager) ranges — a landline gets `carrier: null` because there is no reliable mapping, not because the lookup failed.
- **`location` is an area description**, available for geographic numbers (mostly landlines). Mobile numbers are not tied to a place, so they get `null`.

### Pricing (pay per event)

| Event | Price | When it is charged |
| --- | --- | --- |
| `apify-actor-start` | $0.005 | Charged automatically by the Apify platform when a run starts. Apify covers the compute cost of the first 5 seconds of every run. Never charged from the Actor code |
| `phone-validated` | $0.0005 | Once per **unique** number that got a real verdict — **$0.50 per 1,000** |

**The $0.005 start fee is charged by the Apify platform on every run**, whatever the input turns out to contain — it is what pays for the first 5 seconds of compute, and the Actor never charges it itself. What a run can make free is the **work**: duplicate rows (charged once, for the first occurrence only), blank cells, values that are not phone numbers at all (`N/A`, `ask Marco`, `Weekly digest 12` — they still get a record with the reason), rows with no phone column, files that could not be downloaded, rows dropped by `max_numbers`, and everything after the run's charge limit is reached — the run stops instead of working for free. A run that reads nothing at all costs the $0.005 start fee and not one number.

**And if the file is wrong, you pay cents, not for every broken row:**

- a column that holds no numbers → nothing is read, **$0.005**;
- a national list with no `default_region` → stopped before validating anything, **$0.005**, with the region to set;
- a column of order ids, names or dates → stopped after the first 1,000 values (1,500 when the file is full of duplicates), **under $0.76**, with a diagnosis;
- a list that is simply *old and rotten* — disconnected numbers, wrong lengths, malformed entries that still look like phone numbers — is **not** an accident: that is the job, and it is processed to the last row (as long as at least some of it validates; `ignore_low_validity: true` removes even that condition).

Examples:

- 10,000 rows with 18% duplicates: `$0.005 + 8200 × $0.0005` = **$4.11** (≈ $0.41 per 1,000 uploaded rows)
- 50,000 unique numbers: `$0.005 + 50000 × $0.0005` = **$25.01**
- A daily 500-row incremental check: `$0.005 + 500 × $0.0005` = **$0.26**
- 20,000 rows of junk (wrong column): stopped after the sample, **≤ $0.76**

Compute is a rounding error: parsing, validating, formatting and deduplicating 100,000 numbers takes a few seconds on the default 1 GB memory, because every lookup is a dictionary lookup inside the container. There is no DNS, no SMTP, no HTTP: the per-number price is essentially all you pay.

### Honest comparison

| Alternative | Honest trade-off |
| --- | --- |
| **HLR / network-lookup APIs** (Twilio Lookup, Vonage, …) | They ask the mobile network whether the number is live and where it has been ported: strictly more information than any offline check. They also cost roughly $0.005-0.01 per number — 10-20× this Actor — and they charge for every duplicate and every piece of junk you send them. Use this Actor to clean the list first, then pay for lookups on what survives. |
| **Per-number validator Actors** (one number per input item) | If you already have one clean number and want one answer, they are simpler. They do not take a CSV, do not deduplicate on E.164, do not detect the phone column, do not extract numbers from text and do not segment — which is most of the work when the input is a real CRM export. |
| **`=IF(ISNUMBER(...))` in a spreadsheet** | Free, and how most people do it. It cannot tell a valid mobile from a nine-digit typo, cannot format to E.164, and its "Remove duplicates" will not notice that `0039 333 1234567` is the row above. |
| **Running libphonenumber yourself** | Same engine, same metadata — this Actor is the glue: file parsing, column detection and validation, text extraction, dedup keys, segment exports, the report, and the caps that stop a 200 MB spreadsheet from eating your afternoon. If you have a developer and a day, do it yourself; it is Apache-2.0. |

### Your data

- **You are the data controller.** A contact list is personal data. You decide why it is processed; this Actor is a tool you run inside Apify, and Apify is your processor under its [Data Processing Addendum](https://apify.com/data-processing-addendum). The Actor author is not a party to that processing and never sees your data.
- **No number ever leaves the container.** Validation, line type, carrier, area and time zones all come from metadata files bundled in the image. There is **no third-party API, no enrichment vendor, no LLM and no HLR lookup**, and not a single query of any kind is made about your numbers — the only outbound traffic in the whole Actor is downloading the source URLs *you* provide (which does resolve *that* URL's host, and nothing else). The offline test suite fails on any attempt to open a socket, so this cannot regress unnoticed.
- **Processing is in memory; the output is stored.** Rows are parsed in the container and never written anywhere else — but the dataset records and the CSV files this run produces **persist in your Apify account** under your account's data-retention settings. If your retention policy for contact data is shorter, delete the run's storages when you are done.
- **No number content in the logs, no telemetry.** Logs contain counts, warnings and column names.
- **SSRF-guarded fetching.** Source URLs that resolve to private, loopback, link-local, multicast, reserved, carrier-grade-NAT (`100.64.0.0/10`) or IPv4-mapped-IPv6 addresses are refused, including across redirects.

### Limits and failure behaviour

Nothing here fails a run: every limit produces a warning in the log and in the report. Even an empty or unreadable input ends with a `SUCCEEDED` run and an explicit status message rather than a failure.

| Limit | Value |
| --- | --- |
| Numbers per run | 200,000 (`max_numbers`) |
| Files per run | 20 |
| File size | 50 MB default, 100 MB max |
| Columns per row | 1,024 (only the head of a file is held in memory in full) |
| Characters per cell | 2,000 (a longer cell is truncated with a warning) |
| Dataset record | 8 MB — `source_row` is dropped (with a warning) before that is reached |
| Segment CSV | split into numbered parts, 200,000 rows per segment |
| Circuit breaker | ≥60% not-valid in the first 1,000 unique numbers **and** either no number readable at all, or not one valid number → the run stops with a diagnosis. `ignore_low_validity: true` disables it |

Malformed CSV, unbalanced quotes, broken ZIP/XLSX, PDFs and images (even renamed to `.csv`), unreachable URLs, UTF-16/CP1252/Latin-1 encodings, spreadsheet formulas with no cached value, and files with no phone column all produce a warning and let the rest of the run continue.

### FAQ

**My numbers have no country code. What happens?**
Set `default_region` to the country of the list. If you forget, the run stops before charging for any number and tells you which region would have validated it — you re-run with one setting changed.

**My list mixes several countries.**
Numbers written with `+` are each validated against their own country, whatever `default_region` says. Set `default_region` to the country of the *national* numbers (the ones without a prefix); the summary's `by_region` breakdown shows what you actually got.

**Numbers are inside a notes column, mixed with text.**
Point `phone_column` at it (or let auto-detection find it): each number is extracted, validated and deduplicated like any other, and `phone_original` keeps the full original cell. Only numbers that come out *valid* are taken from text, so `order 12345678 of 2024` is not turned into a contact. If a cell holds several numbers, the first is used and the record says how many were ignored.

**Does it check whether the phone is switched on?**
No, and nothing offline can. See [What "valid" means](#what-valid-means-and-what-it-does-not).

**What is the difference between `valid` and `possible`?**
`possible` = the length is plausible for that country; `valid` = it is also inside a range that country actually assigned. A number that is only *possible* is usually a typo or a very new range. Use `strict: true` to treat those as invalid.

**Why is `carrier` empty for my landlines?**
Because the metadata only maps mobile ranges. An empty carrier is "not available", never "no carrier".

**Can I get only the mobile numbers?**
Download `mobile.csv` from the run's key-value store, or filter the dataset on `number_type`.

**How do I merge several lists?**
Put them all in `sources`: they are read in order, deduplicated *across files*, and `source` + `row_number` in each record tell you where each number came from.

**Does it send an SMS or place a call?**
No. It never contacts a number, a network or an API. The only network traffic is downloading the files you point it at.

**Attribution.** Validation and metadata come from [python-phonenumbers](https://github.com/daviddrysdale/python-phonenumbers), the Python port of Google's [libphonenumber](https://github.com/google/libphonenumber), Apache-2.0 licensed; the licence travels with the library inside the image.

### You may also need

Same author, same principles: you pay for work that was actually done, nothing is stored outside your own run, and no third-party API ever sees your data.

- **[PDF, Word & Excel to Markdown](https://apify.com/yabanana99/pdf-word-excel-to-markdown)** — turn PDFs, Word, Excel and scans into clean Markdown/JSON with cell-level tables and RAG chunks.
- **[Email List Cleaner](https://apify.com/yabanana99/email-list-cleaner-verifier)** — deduplicate, verify and segment a whole email list from CSV or XLSX.

# Actor input Schema

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

Numbers to check, one per item. Use this for quick runs; for real lists use <b>Contact files</b> below. Inline numbers and files are merged and deduplicated together.

## `sources` (type: `array`):

Files to read. Each item is an object with exactly one of: <b>url</b> (http/https link to the file), <b>base64</b> (base64-encoded file content, data URIs accepted) or <b>key</b> (record key in a key-value store, with optional <b>store\_id</b>). Add <b>filename</b> (e.g. <code>contacts.csv</code>) to help format detection. Supported: CSV, TSV, XLSX, JSON, JSON Lines and plain-text lists. Up to 20 files per run.

## `default_region` (type: `string`):

Country to assume for numbers written <b>without</b> a country code (<code>212 555 0123</code>, <code>06 12345678</code>). Two letters, e.g. <code>US</code>, <code>GB</code>, <code>DE</code>, <code>IT</code>, <code>IN</code>, <code>BR</code>. Numbers that already start with <code>+</code> or <code>00</code> ignore it. If your list is national and you leave this empty, the run stops before charging anything and tells you which region to set.

## `phone_column` (type: `string`):

Name of the column (CSV/XLSX header or JSON field) holding the number. Leave empty to auto-detect: known header names first (phone, mobile, telefono, telefon, téléphone…), then the column whose values really are phone numbers. The column actually used is always reported in the run log.

## `dedup` (type: `boolean`):

<b>On</b>: one record per unique number, matched on <b>E.164</b> — so <code>+39 333 1234567</code>, <code>0039 333 1234567</code> and <code>333 1234567</code> are one contact, with <code>occurrences</code> telling how many times it appeared. <b>Off</b>: one record per input row, duplicates flagged with <code>is\_duplicate</code> and <code>duplicate\_of</code>. Either way a duplicate is never charged twice.

## `output_formats` (type: `array`):

Which formats to write for every number. <b>E.164</b> (<code>+393331234567</code>) is what CRMs, SMS gateways and WhatsApp APIs want, and it is always produced because it is also the deduplication key.

## `strict` (type: `boolean`):

A number can have a plausible length for its country and still belong to no assigned range. By default those land in the <b>possible</b> segment (score 60). Turn this on to call them <b>invalid</b> — recommended before an SMS campaign you pay per message.

## `detect_type` (type: `boolean`):

Mobile, fixed line, VoIP, toll-free, premium rate, shared cost, pager, UAN, voicemail. This is what tells you which numbers can receive an SMS and which ones cost money to call.

## `carrier_lookup` (type: `boolean`):

Name of the carrier the number range was <b>originally allocated</b> to, from the metadata bundled in the image — no HLR lookup, no network call. Only mobile ranges are covered. Where the country has mobile number portability the record says so (<code>carrier\_may\_have\_changed</code>), because the subscriber may have moved to another operator.

## `timezone_lookup` (type: `boolean`):

IANA time zones for the number (<code>Europe/Rome</code>), from the bundled metadata. Useful to schedule calls at a sane local hour.

## `geo_lookup` (type: `boolean`):

Geographic description of the number's area (<code>Milan</code>, <code>New York, NY</code>), from the bundled metadata. Available for geographic (mostly fixed-line) numbers only.

## `extract_from_text` (type: `boolean`):

Read the number out of a cell that also contains text (<code>call me on 333 1234567 after 6pm</code>, <code>+39 333 1234567 (home)</code>) instead of calling the row invalid. Only numbers that are <b>valid</b> are taken from text, so an order id in a sentence is never mistaken for a contact.

## `output_segments` (type: `boolean`):

Also write <code>valid.csv</code>, <code>possible.csv</code>, <code>invalid.csv</code> and <code>mobile.csv</code> to the run's key-value store, ready to download and import.

## `include_source_row` (type: `boolean`):

Copy the source row (all its columns) into <code>source\_row</code>, so the cleaned output keeps names, companies and every other field. Up to 30 columns, 500 characters per value and 1000 characters per row.

## `ignore_low_validity` (type: `boolean`):

By default the run stops after the first 1000 numbers if not one of them is a valid phone number (a wrong column, a wrong file or a missing <b>default\_region</b>) — you are charged for the sample, not for the whole file. Turn this on when the list really is that bad and you want every row processed anyway.

## `max_numbers` (type: `integer`):

Safety cap on how many rows are processed (and therefore charged) in one run. Hard limit: 200000.

## `max_file_size_mb` (type: `integer`):

Files larger than this are skipped with a warning instead of failing the run.

## Actor input object example

```json
{
  "phones": [
    "+39 333 1234567",
    "0039 333 1234567",
    "(212) 555-0123",
    "+44 20 7946 0958",
    "call me on 333 1234567 after 6pm",
    "+1 555 555 5555",
    "not a number"
  ],
  "sources": [],
  "default_region": "IT",
  "dedup": true,
  "output_formats": [
    "e164",
    "international",
    "national",
    "rfc3966"
  ],
  "strict": false,
  "detect_type": true,
  "carrier_lookup": true,
  "timezone_lookup": true,
  "geo_lookup": true,
  "extract_from_text": true,
  "output_segments": true,
  "include_source_row": false,
  "ignore_low_validity": false,
  "max_numbers": 200000,
  "max_file_size_mb": 50
}
```

# Actor output Schema

## `numbers` (type: `string`):

One record per number: E.164, international and national formats, validity, line type, region, carrier, timezones, duplicate flag, score and segment. The final \_summary record holds the counts.

## `numbersCsv` (type: `string`):

The same records as CSV, ready to import back into your CRM.

## `segments` (type: `string`):

valid.csv, possible.csv and invalid.csv, downloadable separately.

## `overview` (type: `string`):

Browse the results in the Apify Console.

# 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": [
        "+39 333 1234567",
        "0039 333 1234567",
        "(212) 555-0123",
        "+44 20 7946 0958",
        "call me on 333 1234567 after 6pm",
        "+1 555 555 5555",
        "not a number"
    ],
    "sources": [],
    "default_region": "IT"
};

// Run the Actor and wait for it to finish
const run = await client.actor("yabanana99/phone-number-cleaner-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": [
        "+39 333 1234567",
        "0039 333 1234567",
        "(212) 555-0123",
        "+44 20 7946 0958",
        "call me on 333 1234567 after 6pm",
        "+1 555 555 5555",
        "not a number",
    ],
    "sources": [],
    "default_region": "IT",
}

# Run the Actor and wait for it to finish
run = client.actor("yabanana99/phone-number-cleaner-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": [
    "+39 333 1234567",
    "0039 333 1234567",
    "(212) 555-0123",
    "+44 20 7946 0958",
    "call me on 333 1234567 after 6pm",
    "+1 555 555 5555",
    "not a number"
  ],
  "sources": [],
  "default_region": "IT"
}' |
apify call yabanana99/phone-number-cleaner-validator --silent --output-dataset

```

## MCP server setup

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