# Contact List Cleaner: Fix Phones, Emails & Names, Dedupe (`data-done-right/contact-list-cleaner`) Actor

Clean a contact list: fix name case, tidy emails (syntax and typos), format and validate phone numbers, then remove duplicates. Takes JSON, CSV or a dataset. Offline logic: no mailbox, DNS or phone-line checks. Pay per contact row.

- **URL**: https://apify.com/data-done-right/contact-list-cleaner.md
- **Developed by:** [Data Done Right](https://apify.com/data-done-right) (community)
- **Categories:** Lead generation, Developer tools, Agents
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 contact row cleaneds

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

## Contact List Cleaner: Fix Phones, Emails & Names, Remove Duplicates

**Contact List Cleaner fixes phone numbers, emails and names in a contact list and removes duplicate contacts.** Give it a JSON list, a CSV link or an Apify dataset; get back one tidy row per contact with `+E.164` phone numbers, corrected name capitalisation, cleaned email addresses and a clear list of what was fixed and what is still wrong.

> **What this does NOT do.** It does not check that an email mailbox exists (no DNS lookup, no SMTP, nothing is sent), does not check that a phone line is connected or who owns it, does not enrich contacts, and does not find fuzzy or misspelled duplicates (matches are exact on a normalised key: email, phone, or name plus company). Name splitting is a rule-based heuristic for Western name order; check non-Western names yourself. For mailbox-level email checks use **Verify Email Addresses in Bulk: Syntax, MX & Disposable**.

**What you get**

- Names: `JANE DOE` becomes `Jane Doe`, `doe, jane` becomes `Jane Doe`, `van`/`de` stay lower case, `O'Brien` and `McDonald` are handled, titles (Dr) and suffixes (Jr, III) are split out. Mixed-case names are never touched.
- Emails: lower-cased, extracted from `Name <a@b.com>` or `mailto:`, syntax-checked (international domains work), role accounts flagged (`info@`), known domain typos suggested (`gmial.com` to `gmail.com`).
- Phones: parsed with Google's libphonenumber port (`phonenumbers` 9.0.41), written as E.164, international or national format, with validity, line type (mobile, fixed line, toll free) and country.
- Duplicates: removed or flagged, the most complete row is kept and can be filled from its duplicates.
- Your other columns (id, notes, tags) travel along untouched.

### How to clean a contact list

1. Paste contacts into **Contacts** as a JSON list of objects, or set **CSV file URL**, or pick a **Dataset**. Sources are combined.
2. Column names are detected (`name`, `Full Name`, `email`, `Work Email`, `phone`, `Mobile`, `company`, `First Name`, `Last Name` and similar). If detection is wrong, set the column mapping fields.
3. Set **Default country** (for example `GB`) so numbers written without `+44` are read correctly.
4. Pick how duplicates are found and whether to remove or flag them, then press **Start**.
5. Download the dataset as CSV, Excel or JSON. Views: **Overview**, **Phone details**, **Duplicates**. The `SUMMARY` record has the counts.

### How to format phone numbers to E.164 in bulk

Put the numbers in a `phone` column (or any column and name it in **Phone column**). Set **Default country**. Valid numbers come back in `phone` (in your chosen format) and always in `phoneE164`. Invalid numbers keep their original text, have `phoneValid: false`, and an entry in `issues` telling you why (wrong length, or not valid in that country's numbering plan). A cell with two numbers separated by `;` keeps the first as the phone and the rest in `extraPhones`.

### How to remove duplicate contacts from a list

Choose **Find duplicates by**:

- `email` (default): same address after lower-casing; for Gmail also ignoring dots and `+tags`; `+tags` are also ignored for Outlook, Hotmail, Live, iCloud and Proton.
- `phone`: same valid number in E.164 form. Shared office lines can merge different people, which is why this is not the default.
- `emailOrPhone`, `nameAndCompany`, `any`: matches chain, so A=B by email and B=C by phone puts all three in one group.
- `none`: only clean, no duplicate search.

With **What to do with duplicates** = `remove` only the most complete row of each group is delivered; with `flag` every row is delivered with `isDuplicate` and `duplicateOf`. **Fill gaps from duplicates** copies a missing valid email, phone, name or company from a duplicate to the kept row (listed in `filledFromDuplicates`).

### How to fix names and emails in a CRM export

Send the export as CSV or paste the rows. `fullName`, `firstName`, `lastName`, `prefix` and `nameSuffix` are filled; `changes` lists `name-recased`, `name-reordered`, `email-lowercased`, `email-extracted`, `phone-formatted`; `issues` lists problems such as `email-invalid: needs exactly one @`, `email-domain-possible-typo: did you mean ...`, `phone-invalid: ...`, `name-contains-digits`. Domain typos are only suggested unless you switch on **Auto-correct email domain typos**.

### Input

```json
{
  "contacts": [
    {"name": "JANE DOE", "email": "Jane.Doe@GMAIL.com", "phone": "(415) 555-2671", "company": "Acme Inc."},
    {"name": "doe, jane", "email": "<jane.doe@gmail.com>", "phone": "", "company": "Acme Inc"},
    {"name": "mcdonald, ronald a. jr", "email": "info@mcdonalds-example.con", "phone": "+44 20 7946 0958", "company": "McD Ltd"}
  ],
  "defaultCountry": "US",
  "phoneFormat": "e164",
  "dedupeBy": "email",
  "duplicates": "remove",
  "maxContacts": 1000
}
```

| Field | Default | What it does |
|---|---|---|
| `contacts` | 6 sample rows | JSON list of objects, one per contact. |
| `csvUrl` | empty | Public CSV link (header row, up to 25 MB). Internal/private addresses are blocked. |
| `datasetId` | empty | Apify dataset; each item is one contact. |
| `nameColumn`, `firstNameColumn`, `lastNameColumn`, `emailColumn`, `phoneColumn`, `companyColumn` | auto-detect | Override the detected column (case and punctuation are ignored). |
| `defaultCountry` | `US` | ISO country for numbers without `+`. |
| `phoneFormat` | `e164` | `e164`, `international` or `national`. |
| `fixNameCase` | true | Recase ALL CAPS / all lower case names. |
| `fixEmailTypos` | false | Apply known domain typo fixes (otherwise only suggested). |
| `dedupeBy` | `email` | `email`, `phone`, `emailOrPhone`, `nameAndCompany`, `any`, `none`. |
| `duplicates` | `remove` | `remove` or `flag`. |
| `mergeDuplicates` | true | Fill gaps of the kept row from its duplicates. |
| `maxContacts` | 1000 | Cap on non-empty rows processed and charged (1 to 200,000). |

### Output

Real output of the sample input (the input has 6 rows: 1 blank, 5 contacts; shown here are two of the 4 delivered rows, `Jane Doe` merged with her duplicate):

```json
{
  "rowNumber": 1, "firstName": "Jane", "lastName": "Doe", "fullName": "Jane Doe",
  "email": "jane.doe@gmail.com", "emailValid": true, "emailDomain": "gmail.com", "emailIsRole": false,
  "phone": "+14155552671", "phoneE164": "+14155552671", "phoneValid": true, "phoneType": "fixed_line_or_mobile", "phoneCountry": "US",
  "company": "Acme Inc.", "issues": [], "changes": ["name-recased", "email-lowercased", "phone-formatted"],
  "isDuplicate": false, "duplicatesFound": 1, "duplicateRows": [2], "filledFromDuplicates": []
}
{
  "rowNumber": 3, "firstName": "Ronald", "middleName": "A.", "lastName": "McDonald", "nameSuffix": "Jr", "fullName": "Ronald A. McDonald Jr",
  "email": "info@mcdonalds-example.con", "emailValid": true, "emailIsRole": true, "emailSuggestion": "info@mcdonalds-example.com",
  "phone": "+442079460958", "phoneValid": true, "phoneType": "fixed_line", "phoneCountry": "GB", "company": "McD Ltd",
  "issues": ["email-domain-possible-typo: did you mean info@mcdonalds-example.com"],
  "changes": ["name-reordered", "name-recased", "phone-formatted"]
}
```

(Shortened: unchanged null fields are left out of this README; the real rows contain every field.) Your own extra columns are added to each row under their own names; a column that collides with an output field is renamed `input_<name>`. `emailValid: true` means the syntax is valid; a flagged typo domain can still be syntactically valid.

### How much does it cost

Pay per event: **$0.0015 per contact row cleaned** (event `contact-row-cleaned`, $1.50 per 1,000). A row is charged when it is processed, including rows later removed as duplicates; completely empty rows are free. Lower prices apply on higher Apify plans.

| Rows | Cost |
|---|---|
| 1,000 | $1.50 |
| 10,000 | $15.00 |
| 100,000 | $150.00 |

`maxContacts` caps the spend, and the Apify run limit is respected: the run stops and says "spending limit" in its status.

### Use cases

- Clean a CRM or spreadsheet export before importing it into HubSpot, Salesforce or a mailing tool.
- Convert mixed-format phone numbers to E.164 for an SMS or dialer import.
- Merge duplicate signups from several forms into one row per person.
- Prepare scraped lead lists: tidy names, drop invalid emails and phones, keep source columns.
- Add a cleaning step in an n8n, Make or Zapier flow, or let an AI agent call it through MCP.

### FAQ

**Does it verify that an email exists?** No. It checks syntax, flags role accounts and suggests fixes for known domain typos. No DNS or mailbox check is made, nothing is sent.

**Does `phoneValid: true` mean the number is in use?** No. It means the number fits a real numbering plan (libphonenumber rules, version 9.0.41 at build time). It can still be disconnected. The library's data lags new number ranges.

**How are names handled?** Names that are ALL CAPS or all lower case are recased token by token; mixed-case tokens are left alone. `Last, First` with a comma is swapped; `Smith John` without a comma is not (it cannot be told apart from a real name). First name is the first word, last name the last word plus particles like `van`.

**Can it find near-duplicates like `Jon` and `John`?** No, matching is exact on normalised keys. Use the `nameAndCompany` mode for same name and company after case, accent and company-suffix (Inc, Ltd, GmbH) normalisation.

**Which row is kept among duplicates?** The row with the most usable data (name, valid email, valid phone, company); the earlier row wins ties.

**What if my CSV link is private?** Public links only. Private or internal addresses are blocked and the run notes it in `SUMMARY`.

### Use with the Apify API / MCP / n8n / Make

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("<username>/contact-list-cleaner").call(run_input={
    "contacts": [{"name": "JANE DOE", "email": "Jane@Acme.com", "phone": "(415) 555-2671"}],
    "defaultCountry": "US",
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["fullName"], item["email"], item["phoneE164"])
```

```javascript
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<APIFY_TOKEN>' });
const run = await client.actor('<username>/contact-list-cleaner').call({
  contacts: [{ name: 'JANE DOE', email: 'Jane@Acme.com', phone: '(415) 555-2671' }],
  defaultCountry: 'US',
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

In n8n or Make use the Apify node/module "Run Actor" with the same input and read the dataset. AI agents can call it through the Apify MCP server.

### Data and credits

Phone parsing uses the open-source `phonenumbers` package (Apache 2.0, a port of Google's libphonenumber). No external data source or list is downloaded at run time except the CSV you point to.

### Related tools

- **Remove Duplicates: Deduplicate Datasets, CSV & Excel**: generic duplicate removal on any dataset.
- **Phone Number Validation & Lookup: Format, Carrier, Line Type**: phone-only validation with carrier and time zones.
- **Verify Email Addresses in Bulk: Syntax, MX & Disposable**: MX and disposable-domain checks for the emails you keep.
- **Email Extractor from Text: Emails, Phones & URLs from Any Text**: pull contacts out of raw text first.

# Actor input Schema

## `contacts` (type: `array`):

Contacts as a JSON list of objects, one per person. Example: \[{"name": "JANE DOE", "email": "Jane@Acme.com", "phone": "(415) 555-2671"}]. Extra keys are kept untouched in the output.

## `csvUrl` (type: `string`):

Public link to a CSV file with a header row (up to 25 MB). Example: https://example.com/contacts.csv. Public files only, no logins; internal addresses are blocked.

## `datasetId` (type: `string`):

Pick an Apify dataset (for example a scraper's output). Every item is treated as one contact. Example: a dataset ID such as aBcD1234eFgH5678i.

## `nameColumn` (type: `string`):

Column that holds the full name. Leave empty to detect it (name, full name, contact). Example: Contact Name.

## `firstNameColumn` (type: `string`):

Column with the first name, used with the last name column instead of a full name. Example: First Name. Leave empty to detect.

## `lastNameColumn` (type: `string`):

Column with the last name. Example: Surname. Leave empty to detect.

## `emailColumn` (type: `string`):

Column with the email address. Example: Work Email. Leave empty to detect.

## `phoneColumn` (type: `string`):

Column with the phone number. Example: Mobile. Leave empty to detect.

## `companyColumn` (type: `string`):

Column with the company name. Example: Organisation. Leave empty to detect.

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

Two-letter ISO code used for numbers written without +country code. Example: US, GB, DE, FR. Default: US. Numbers that start with + ignore this.

## `phoneFormat` (type: `string`):

How valid phone numbers are written in the phone field. phoneE164 is always filled for valid numbers. Default: E.164.

## `fixNameCase` (type: `boolean`):

Recase names written in ALL CAPS or all lower case (JANE DOE becomes Jane Doe; van and de stay lower case). Mixed-case names such as McDonald are never touched. Default: on.

## `fixEmailTypos` (type: `boolean`):

Replace known domain typos (gmial.com, hotmial.com, .con) with the suggested address. Off by default: the suggestion is always shown in emailSuggestion and nothing is changed.

## `dedupeBy` (type: `string`):

Which match makes two rows duplicates. Email ignores case, gmail dots and +tags. Phone compares valid numbers in E.164 form. Shared office numbers can merge different people, so phone matching is opt-in.

## `duplicates` (type: `string`):

remove: only the most complete row of each duplicate group is delivered. flag: all rows are delivered with isDuplicate and duplicateOf. Rows are charged either way. Default: remove.

## `mergeDuplicates` (type: `boolean`):

When the kept row lacks a valid email, phone, name or company that a duplicate has, copy it over (see filledFromDuplicates). Default: on.

## `maxContacts` (type: `integer`):

Upper limit of non-empty rows to process and charge. Example: 1000. Rows beyond it are skipped and not charged.

## Actor input object example

```json
{
  "contacts": [
    {
      "name": "JANE DOE",
      "email": "Jane.Doe@GMAIL.com",
      "phone": "(415) 555-2671",
      "company": "Acme Inc."
    },
    {
      "name": "doe, jane",
      "email": "<jane.doe@gmail.com>",
      "phone": "",
      "company": "Acme Inc"
    },
    {
      "name": "mcdonald, ronald a. jr",
      "email": "info@mcdonalds-example.con",
      "phone": "+44 20 7946 0958",
      "company": "McD Ltd"
    },
    {
      "name": "ludwig van beethoven",
      "email": "ludwig@gmial.com",
      "phone": "12345",
      "company": "Bonn Music"
    },
    {
      "name": "Priya Nair",
      "email": "mailto:priya.nair@outlook.com; priya@example.org",
      "phone": "+44 20 7946 0018",
      "company": "Nair & Co"
    },
    {
      "name": "",
      "email": "",
      "phone": "",
      "company": ""
    }
  ],
  "defaultCountry": "US",
  "phoneFormat": "e164",
  "fixNameCase": true,
  "fixEmailTypos": false,
  "dedupeBy": "email",
  "duplicates": "remove",
  "mergeDuplicates": true,
  "maxContacts": 1000
}
```

# Actor output Schema

## `results` (type: `string`):

One row per contact: cleaned name, email, phone, company, fixes, issues and duplicate info, plus your own extra columns (all items as JSON, CSV, Excel and more).

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

SUMMARY record: rows in and charged, detected columns, counts of valid and invalid emails and phones, fixes, duplicates, notes.

## `records` (type: `string`):

Key-value store records of this run.

# 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 = {
    "contacts": [
        {
            "name": "JANE DOE",
            "email": "Jane.Doe@GMAIL.com",
            "phone": "(415) 555-2671",
            "company": "Acme Inc."
        },
        {
            "name": "doe, jane",
            "email": "<jane.doe@gmail.com>",
            "phone": "",
            "company": "Acme Inc"
        },
        {
            "name": "mcdonald, ronald a. jr",
            "email": "info@mcdonalds-example.con",
            "phone": "+44 20 7946 0958",
            "company": "McD Ltd"
        },
        {
            "name": "ludwig van beethoven",
            "email": "ludwig@gmial.com",
            "phone": "12345",
            "company": "Bonn Music"
        },
        {
            "name": "Priya Nair",
            "email": "mailto:priya.nair@outlook.com; priya@example.org",
            "phone": "+44 20 7946 0018",
            "company": "Nair & Co"
        },
        {
            "name": "",
            "email": "",
            "phone": "",
            "company": ""
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("data-done-right/contact-list-cleaner").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 = { "contacts": [
        {
            "name": "JANE DOE",
            "email": "Jane.Doe@GMAIL.com",
            "phone": "(415) 555-2671",
            "company": "Acme Inc.",
        },
        {
            "name": "doe, jane",
            "email": "<jane.doe@gmail.com>",
            "phone": "",
            "company": "Acme Inc",
        },
        {
            "name": "mcdonald, ronald a. jr",
            "email": "info@mcdonalds-example.con",
            "phone": "+44 20 7946 0958",
            "company": "McD Ltd",
        },
        {
            "name": "ludwig van beethoven",
            "email": "ludwig@gmial.com",
            "phone": "12345",
            "company": "Bonn Music",
        },
        {
            "name": "Priya Nair",
            "email": "mailto:priya.nair@outlook.com; priya@example.org",
            "phone": "+44 20 7946 0018",
            "company": "Nair & Co",
        },
        {
            "name": "",
            "email": "",
            "phone": "",
            "company": "",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("data-done-right/contact-list-cleaner").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 '{
  "contacts": [
    {
      "name": "JANE DOE",
      "email": "Jane.Doe@GMAIL.com",
      "phone": "(415) 555-2671",
      "company": "Acme Inc."
    },
    {
      "name": "doe, jane",
      "email": "<jane.doe@gmail.com>",
      "phone": "",
      "company": "Acme Inc"
    },
    {
      "name": "mcdonald, ronald a. jr",
      "email": "info@mcdonalds-example.con",
      "phone": "+44 20 7946 0958",
      "company": "McD Ltd"
    },
    {
      "name": "ludwig van beethoven",
      "email": "ludwig@gmial.com",
      "phone": "12345",
      "company": "Bonn Music"
    },
    {
      "name": "Priya Nair",
      "email": "mailto:priya.nair@outlook.com; priya@example.org",
      "phone": "+44 20 7946 0018",
      "company": "Nair & Co"
    },
    {
      "name": "",
      "email": "",
      "phone": "",
      "company": ""
    }
  ]
}' |
apify call data-done-right/contact-list-cleaner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,data-done-right/contact-list-cleaner"
        }
    }
}
```

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/Li6atWF034XRULKrw/builds/GAIH0LXspXsEtc1WA/openapi.json
