Phone Number Normalization E.164 avatar

Phone Number Normalization E.164

Pricing

from $0.25 / 1,000 normalized phone rows

Go to Apify Store
Phone Number Normalization E.164

Phone Number Normalization E.164

Phone lists from forms, scrapers, CRM exports and spreadsheets are messy: mixed formats, 00/011 prefixes, '(0)' trunk digits, Excel scientific-notation damage, several numbers in one cell, and duplicates. Automation users (n8n, Make, Zapier, Clay, CRM) need every row returned with a validated...

Pricing

from $0.25 / 1,000 normalized phone rows

Rating

0.0

(0)

Developer

Jordan Nabbe

Jordan Nabbe

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

8 days ago

Last modified

Share

Clean phone lists offline before you import or message them. This Actor is built for RevOps and lead-gen teams, WhatsApp and SMS campaign operators, and n8n, Make, Zapier and Clay users. It reads your own contact list as inline records, CSV text, JSON text or a real CSV, JSON or XLSX file, and repairs common spreadsheet damage. Every phone value it can read is converted to E.164 plus national, international, RFC 3966 and WhatsApp formats, and duplicate numbers are flagged. All original columns are kept, so the rows go straight back into a CRM, spreadsheet, database or LLM pipeline. Numbers are processed inside the run and never sent to a lookup service.

What it does

  • Reads inline objects, CSV text, JSON text and base64-encoded CSV, JSON or XLSX files. XLSX workbooks are parsed from the actual file bytes with ExcelJS. CSV is parsed with Papa Parse, including quoted fields.
  • Converts national and international notations into E.164, national, international and RFC 3966 strings, plus WhatsApp digits and a wa.me link. The link is generated but never opened.
  • Takes each row's country from a column you choose (ISO code, English country name or calling code) and falls back to a default country. An explicit international prefix always takes precedence: +, 00, or 011 on US and Canadian rows.
  • Handles 00 and 011 prefixes, (0) trunk digits, extensions written as ext, x, # or ;ext=, and a leading apostrophe. Spreadsheet repairs are listed in the row's warnings: ="..." text wrappers, a trailing .0, exact scientific notation and a country code without +.
  • Splits several numbers in one cell, separated by ; , | or line breaks. The first valid number becomes the primary one. Other valid numbers go to additional_numbers and unreadable ones to candidate_errors.
  • Flags duplicates on the primary E.164 number with duplicate_group_id and is_first_seen, or keeps only the first occurrence.
  • Keeps every original column in original and adds deterministic snake_case keys in normalized_original.
  • Can also save CLEANED.csv in the run's key-value store.
  • Handles failures per row. Empty, invalid or unprocessable values never stop the run. They are listed with a reason_code and message in the rejected array of the OUTPUT record. Only rows with a valid number go to the dataset.

National-format parsing and structural checks cover the United States, Canada, the United Kingdom, Germany, France, Spain, Italy, the Netherlands, Australia and India. This build does not bundle libphonenumber metadata. As a result, validation_level is always structure_only, line_type is always null and time_zones is always empty.

How to use it

  1. Click Start. The prefilled example runs as-is and shows valid, duplicate and rejected rows.
  2. For your own list, replace Contact records, or clear it and paste CSV text, JSON text or a base64 file instead. Use exactly one source per run.
  3. Enter your phone columns and country column exactly as they appear in the header. Then choose a default country and how to handle duplicates.

Copy-paste input:

{
"records": [
{ "name": "Ana", "country": "GB", "phone": "(0)20 7946 0958" },
{ "name": "Bob", "country": "DE", "phone": "00 49 30 123456" },
{ "name": "Ana duplicate", "country": "GB", "phone": "+44 20 7946 0958" },
{ "name": "Needs review", "country": "US", "phone": "123" }
],
"phoneColumns": ["phone"],
"countryColumn": "country",
"defaultCountry": "US",
"dedupe": "flag",
"saveCleanedFile": true
}

For an XLSX workbook, base64-encode the original file bytes, put the text in fileBase64 and set fileFormat to xlsx. If you leave out phoneColumns, the Actor uses every column whose name contains phone, mobile, tel, cell or whatsapp.

Output

The default dataset holds one row for each input row that produced a valid number:

row_indexsource_referenceinput_valuee164nationalduplicate_group_idis_first_seen
0inline:records(0)20 7946 0958+442079460958020 7946 0958g1true
1inline:records00 49 30 123456+4930123456030 123456nulltrue
2inline:records+44 20 7946 0958+442079460958020 7946 0958g1false

Each row also contains fields like these:

{
"phone_column": "phone",
"country": "GB",
"calling_code": "44",
"international": "+44 20 7946 0958",
"rfc3966": "tel:+442079460958",
"whatsapp_digits": "442079460958",
"wa_link": "https://wa.me/442079460958",
"extension": null,
"additional_numbers": [],
"is_valid": true,
"validation_level": "structure_only",
"reason_code": "OK",
"status": "ok",
"warnings": [],
"original": { "country": "GB", "name": "Ana", "phone": "(0)20 7946 0958" }
}

The fourth input row (value 123) is not in the dataset. It appears in OUTPUT.rejected with reason_code TOO_SHORT and a readable message.

The OUTPUT record also holds:

  • counters: processed, valid, invalid, errors, duplicates and skipped
  • the delivered rows
  • an outcome object with status (SUCCEEDED, PARTIAL, FAILED or LIMIT_REACHED), itemsReceived, itemsSucceeded, itemsFailed, billableCount, limitReached and undelivered

Every row carries source_reference and a zero-based row_index. For XLSX files, row_index + 2 is the worksheet row.

Pricing

Pay per event. The main charge is per normalized phone row; everything else listed below is free.

EventPrice (USD)Charged when
Run start (apify-actor-start)$0.002The Actor run starts.
Normalized phone row (apify-default-dataset-item)$0.00025A row with a structurally valid E.164 number is pushed to the dataset.

Not charged:

  • OUTPUT summary and outcome
  • rejected rows listed in OUTPUT
  • CLEANED.csv

Examples (default 512 MB memory, one start event):

  • 1,000 valid rows: $0.002 start + 1 × $0.002 + 1000 × $0.00025 = $0.254
  • Only rejected rows: $0.002 start + 1 × $0.002 + 0 × $0.00025 = $0.004

Spending limit: when you set a maximum cost per run, the Actor stops before it would exceed it, keeps every normalized phone row it already delivered and reports limitReached in the OUTPUT record. It never delivers results beyond the limit.

The Store's Pricing tab shows the prices in force. If this section and the Pricing tab ever differ, the Pricing tab applies.

Use cases

  • Convert a CRM or spreadsheet export to E.164 before import.
  • Clean and dedupe a lead list before a WhatsApp or SMS campaign.
  • Add an offline normalization step to an n8n, Make, Zapier or Clay workflow through the Apify API.
  • Read national-format numbers correctly using a per-row country column.
  • Send rejected values to a manual review queue using OUTPUT.rejected.

Limits

  • At most 1,000 rows per run, in every format. maxRows processes only the first rows; the rest are counted as skipped.
  • Size limits:
    • total input: 4 MB
    • CSV or JSON text: 1 MB
    • decoded base64 file: 2 MB
    • parsed rows: 2 MB of JSON
    • JSON nesting depth: 8
    • keys per object: 100
  • Up to 10 phone columns and 10 numbers per cell. A cell with more numbers is reported as a row error instead of being cut off without notice.
  • XLSX files:
    • only the first worksheet is read
    • row 1 must hold unique, nonempty headers
    • up to 100 columns
    • archive expansion is limited to 8 MB
    • formulas are never evaluated; cached results are used when present
  • This build cannot download files from URLs. Supply files as base64.
  • The structural rules are deliberately small. They can reject legitimate special-service numbers or accept unassigned ones.
  • A +1 number alone cannot tell the US from Canada, so country stays empty unless the row says US or CA.
  • Digits lost to spreadsheet rounding cannot be recovered.
  • Ambiguous values are never guessed. Letters, lost precision, fractional scientific notation and unknown countries are rejected with an explicit reason_code.

FAQ

Does is_valid mean the number is active?

No. It means the value meets this build's structural rules for its country. The Actor does not check whether a number is reachable or who owns it, and it does not check carrier assignment, line type or consent.

Are numbers sent to a lookup service?

No. Everything is processed inside the run, with no network requests. Your input and results stay in your own Apify storage, so set retention to suit your data policy.

How are duplicates defined?

Only the primary E.164 number is compared, and extensions are ignored. With flag, every row is kept and marked with duplicate_group_id and is_first_seen. With keepFirst, later matching rows are removed, including any different additional numbers they carry.

What happens to bad data?

Invalid rows are listed in OUTPUT.rejected and never enter the dataset. The run fails in three cases: no valid number is produced, the input is malformed, or the input is over a limit. The Actor first writes OUTPUT with a FAILED outcome and the reason, so a failure is never reported as success.

Why does the cleaned CSV show a leading apostrophe?

In CLEANED.csv, values that start with a formula character are escaped to protect spreadsheet imports. This includes the plus sign at the start of an E.164 number. Dataset and OUTPUT values are unchanged.