Phone Number Validator & Email Cleaner: Contact Normalizer avatar

Phone Number Validator & Email Cleaner: Contact Normalizer

Pricing

Pay per event

Go to Apify Store
Phone Number Validator & Email Cleaner: Contact Normalizer

Phone Number Validator & Email Cleaner: Contact Normalizer

Validate and format phone numbers (E.164, type, region) and clean emails, names and countries in bulk for CRM imports, lead lists and AI agents. Flags disposable, role and invalid emails, dedupes records. Offline, $0.0005 per record.

Pricing

Pay per event

Rating

0.0

(0)

Developer

tinplatelabs

tinplatelabs

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

9 hours ago

Last modified

Share

Phone Number Validator and Email Cleaner: Contact Data Normalizer

Validate and format phone numbers (E.164, national and international, with country and line type), check email addresses (syntax, disposable, role and free-provider flags), tidy names, convert countries to ISO codes and find duplicates, offline, in bulk, for $0.0005 per record.

For CRM imports, lead lists, form exports and scraped contact data, and for AI agents (Claude, ChatGPT, Cursor and others through the Apify MCP server) that need contact data in one consistent shape before they act on it.

Use cases

  • Phone number validation and formatting: (650) 253-0000, 020 7946 0958 and +1 650-253-0000 become E.164.
  • Email list cleaning: invalid syntax, disposable domains (mailinator...) and role accounts (info@) flagged.
  • CRM import and dedupe: one consistent record shape, duplicates by email and/or phone marked or dropped.
  • Lead-scraper output: pick the dataset of another Apify Actor and clean it in place of a CSV.
  • AI agents: read normalized for clean values and issues for what's wrong, with a plain-English fix.

Quick start

This input (the Actor's example) cleans four records in about 4 seconds for $0.002:

{
"records": [
{"name": "ada LOVELACE", "email": " Ada.Lovelace@EXAMPLE.com ", "phone": "(650) 253-0000", "country": "USA"},
{"name": "JEAN-LUC O'BRIEN", "email": "info@mailinator.com", "phone": "020 7946 0958", "country": "United Kingdom"},
{"name": "Ada Lovelace", "email": "ada.lovelace@example.com", "phone": "+1 650-253-0000", "country": "us"},
{"name": "grace hopper", "email": "grace@@example", "phone": "+1 555 12", "country": "Atlantis"}
],
"defaultCountry": "US",
"dedupe": "emailOrPhone"
}

One dataset item per record (abridged, real output from 2026-10-24):

{"index": 1, "status": "ok", "valid": true,
"normalized": {"name": "Jean-Luc O'Brien", "email": "info@mailinator.com", "phone": "+442079460958", "country": "GB"},
"issues": [
{"field": "email", "code": "EMAIL_DISPOSABLE", "severity": "warning", "message": "mailinator.com is a disposable (throwaway) mail domain."},
{"field": "email", "code": "EMAIL_ROLE_ACCOUNT", "severity": "warning", "message": "'info@' is a role account that usually reaches a team, not a person."}
],
"isDuplicate": false}

Record 2 comes back with "isDuplicate": true, "duplicateOf": 0, and record 3 with "valid": false and EMAIL_INVALID, PHONE_INVALID and COUNTRY_UNKNOWN issues.

Pricing in plain words

You pay $0.0005 per record ($0.50 per 1,000). Records that can't be read (not an object, no contact fields) and dropped duplicates cost nothing, and maxRecords caps the cost of a run. Full table under Pricing.

Why this one

  • Offline and private. Phone checks use Google's libphonenumber metadata and email checks are pure syntax rules plus vendored lists. No carrier APIs, no LLMs, no telemetry. DNS is touched only if you turn on checkMx.
  • Cheap and fast. One run handles thousands of records per second at 256 MB memory. $0.50 per 1,000 records.
  • Deterministic. The same input always gives the same output, so you can diff and re-run safely.
  • Everything in one pass: phones to E.164 with national/international formats, region and line type; emails with RFC syntax validation, normalization, disposable/role/free-provider flags and optional Gmail canonical form; names tidied; countries to ISO 3166-1 codes; optional dedupe by email and/or phone.
  • Agent-shaped output. Each item has the untouched original, a drop-in normalized record (same keys, clean values), per-field fields details and an issues list with codes, severities and plain-English fixes.

Input

Give records inline, an Apify dataset, or a CSV/JSON file URL (or a mix; they are processed in that order).

FieldTypeDefaultWhat it does
recordsarray of objectsInline records. Other columns are copied through.
datasetIdstringApify dataset to read, e.g. the output of a lead-scraping Actor. Pick it in Console or pass its ID (or username~dataset-name). The Actor runs with limited permissions: it can read only the dataset you give here.
dataUrlstringPublic CSV (header row; , ; tab or `
fieldMappingobjectdetect{"email": [...], "phone": [...], "name": [...], "country": [...]} column lists. Empty = detect by column name.
defaultCountrystringUSCountry for phones written without +country code. ISO code or name. Empty = require +.
useRecordCountrybooleantrueUse the record's own country field for its phones.
phoneFormate164 | international | nationale164Format written into normalized.
countryFormatalpha2 | alpha3 | namealpha2Format written into normalized.
lowercaseEmailbooleantrueLowercase the local part too (the domain is always lowercased).
canonicalizeEmailbooleanfalseAdd canonical: Gmail dots and +tags removed, +tags removed for Outlook/iCloud/Fastmail/Proton. Used by dedupe.
checkMxbooleanfalseDNS MX lookup per email domain (cached per domain).
dedupenone | email | phone | emailOrPhonenoneMark duplicates across the whole run.
dropDuplicatesbooleanfalseLeave duplicates out of the output (and the bill).
maxRecordsinteger1,000,000Stop after this many records.

Column detection: email, e-mail, Email Address → email; phone, mobile, tel, cell, fax, whatsapp, mobilePhone → phone; name, first name, last_name, surname, contact name → name (but not company name, username, file name...); anything containing country → country.

{
"records": [
{"name": "ada LOVELACE", "email": " Ada.Lovelace@EXAMPLE.com ", "phone": "(650) 253-0000", "country": "USA", "company": "Example Inc"},
{"name": "grace hopper", "email": "info@mailinator.com", "phone": "+1 555 12", "country": "Atlantis"}
],
"defaultCountry": "US",
"dedupe": "emailOrPhone"
}

Output

One dataset item per input record, in input order:

{
"index": 0,
"status": "ok",
"valid": true,
"original": {"name": "ada LOVELACE", "email": " Ada.Lovelace@EXAMPLE.com ", "phone": "(650) 253-0000", "country": "USA", "company": "Example Inc"},
"normalized": {"name": "Ada Lovelace", "email": "ada.lovelace@example.com", "phone": "+16502530000", "country": "US", "company": "Example Inc"},
"fields": {
"name": {"kind": "name", "input": "ada LOVELACE", "normalized": "Ada Lovelace", "recased": true},
"email": {"kind": "email", "input": " Ada.Lovelace@EXAMPLE.com ", "valid": true, "normalized": "ada.lovelace@example.com",
"canonical": null, "local": "ada.lovelace", "domain": "example.com",
"disposable": false, "role": false, "freeProvider": false, "mx": null},
"phone": {"kind": "phone", "input": "(650) 253-0000", "valid": true, "possible": true, "e164": "+16502530000",
"national": "(650) 253-0000", "international": "+1 650-253-0000", "region": "US", "countryCode": 1,
"type": "fixed_line_or_mobile", "extension": null},
"country": {"kind": "country", "input": "USA", "valid": true, "alpha2": "US", "alpha3": "USA", "numeric": "840", "name": "United States"}
},
"issues": [],
"isDuplicate": false,
"duplicateOf": null
}

The second example record comes back with "valid": false and these issues:

[
{"field": "email", "code": "EMAIL_DISPOSABLE", "severity": "warning", "message": "mailinator.com is a disposable (throwaway) mail domain."},
{"field": "email", "code": "EMAIL_ROLE_ACCOUNT", "severity": "warning", "message": "'info@' is a role account that usually reaches a team, not a person."},
{"field": "phone", "code": "PHONE_INVALID", "severity": "error", "message": "'+1 555 12' is not a valid number for country code +1 (wrong length or unassigned range). Check the digits and the country."},
{"field": "country", "code": "COUNTRY_UNKNOWN", "severity": "error", "message": "'Atlantis' is not a recognised country. Use an ISO 3166 code (e.g. 'DE', 'DEU') or an English name."}
]
  • valid is false when any issue has severity error. Warnings (disposable, role account, duplicate, MX lookup failed) leave it true.
  • Invalid values stay in normalized as the trimmed input, so no data is lost.
  • Phone type: mobile, fixed_line, fixed_line_or_mobile (US/Canada numbers can't be told apart offline), toll_free, premium_rate, shared_cost, voip, personal_number, pager, uan, voicemail, unknown.
  • Issue codes: PHONE_UNPARSEABLE, PHONE_NO_COUNTRY, PHONE_INVALID, EMAIL_INVALID, EMAIL_DISPOSABLE, EMAIL_ROLE_ACCOUNT, EMAIL_NO_MX, EMAIL_MX_LOOKUP_FAILED, COUNTRY_UNKNOWN, DUPLICATE, and for status: "error" items NOT_AN_OBJECT and NO_CONTACT_FIELDS. A dataset that can't be read gives one error item (index: null, source: "datasetId") with DATASET_ACCESS_DENIED, DATASET_NOT_FOUND or DATASET_READ_FAILED and the fix; the other inputs are still processed and the error item is not charged.
  • The key-value store record OUTPUT holds a run summary: records, normalized, valid, withIssues, errors, duplicates, duplicatesDropped, charged, stoppedReason.

Export the dataset as CSV or Excel from Apify to get flat columns such as normalized.email.

Pricing (pay per event)

EventPriceWhen
record-normalized$0.0005Each record processed (including records with validation issues, which are the result you asked for)
Actor startset by ApifyPer run

10,000 records cost $5. Error items (a record that isn't an object, or has no contact fields) and dropped duplicates are free. If the run's maximum total charge is reached, the Actor stops cleanly and OUTPUT says so.

Limits

  • Up to 1,000,000 records per run; dataUrl files up to 200 MB (streamed: 1M rows run in 256 MB).
  • A file problem never loses the rest of the run: an unreadable, malformed or unsupported file (SOURCE_UNREADABLE, FILE_TOO_LARGE, UNSUPPORTED_FILE, CSV_MALFORMED, JSON_INVALID) becomes one error item and records read before it are kept. If memory runs low the run stops cleanly (MEMORY_LOW) and says to raise it. Error items are never charged; the run's status message summarizes the result.
  • Phone line type comes from numbering-plan metadata, not a live carrier lookup: ported numbers keep their original type, and there is no carrier name or reachability check.
  • Email checks are syntax plus lists; checkMx confirms the domain accepts mail but never contacts a mailbox (no SMTP probing).
  • Names are tidied, not split or gender-guessed. Casing is changed only when it is clearly mechanical (ALL CAPS or all lowercase words); hand-cased names like DeLuca are left alone.
  • Disposable-domain list as of the release date (see licenses); new throwaway domains appear daily.

Privacy

Records are processed inside your run and written only to that run's dataset and key-value store, which follow your Apify data-retention settings. Nothing is sent to any third party and no LLM sees your data. The only exception is the optional checkMx check, which sends each email domain (never the full address) to DNS resolvers. With dataUrl, the Actor downloads only that exact file.

Use from an AI agent (MCP)

Add the Apify MCP server to your agent and enable this Actor. Call it with {"records": [...]} and read normalized for clean values and issues for what to fix or ask the user about.

Open-source licenses

All dependencies are permissively licensed (no GPL/LGPL; pycountry is deliberately not used). CI fails on any copyleft dependency.

ComponentLicenseRole
phonenumbers 9.0.41 (libphonenumber metadata)Apache-2.0Phone parsing, formatting, type
email-validator 2.3.0UnlicenseEmail syntax, normalization, optional MX check
dnspython 2.9.0, idna 3.20ISC / BSD-3-ClauseDNS (MX check only), internationalized domains
iso3166 3.0.0MITISO 3166-1 country codes and names
disposable-email-domains list (vendored, 2026-10-10)CC0-1.0Disposable domain flag
httpx 0.28.1, httpcore, h11, anyioBSD-3-Clause / MITdataUrl download
Apify SDK for Python 4.1.0, crawleeApache-2.0Actor runtime

The free-provider and role-account lists and the country aliases were written for this Actor.