# 🧹 Contact & Company Deduplicator - Clean Your CRM List (`that_red_bird/contact-deduplicator`) Actor

🧹 Upload one messy contact or company list and get back a clean, de-duplicated list ready to re-import into your CRM. ✅ Fuzzy name/email/phone/company matching, sensible zero-config defaults, plus a duplicates-to-review list and a full merge audit log explaining what was merged and why.

- **URL**: https://apify.com/that\_red\_bird/contact-deduplicator.md
- **Developed by:** [mohamed alaya](https://apify.com/that_red_bird) (community)
- **Categories:** Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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/actors/running/actors-in-store.md#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

## Contact & Company Deduplicator

Upload one messy contact or company list — exported from a CRM, a spreadsheet, a form tool,
wherever — and get back a clean list with the duplicates merged, ready to re-import. No shared
ID required: it figures out that "Jennifer Adams / Acme Corporation" and "J. Adams / ACME, Inc."
are the same person from the data itself.

This is the simpler, single-list sibling of the Entity Resolution Engine actor. Same matching
engine underneath (fuzzy name/company matching, blocking, probabilistic scoring, transitive
clustering, survivorship), but zero-config by default and with output shaped specifically for
CRM cleanup instead of general entity resolution.

### What it does

1. Auto-detects which columns are name, email, phone, company and address (or you can specify
   them yourself).
2. Groups obviously-unrelated rows apart first ("blocking"), so a 20,000-row list doesn't need
   200 million comparisons.
3. Scores every remaining pair on weighted field similarity — an email match counts for much
   more than two people happening to share a surname.
4. Merges everything that scores above the match threshold, even transitively (A~B and B~C
   means A, B and C become one contact), and reports anything in between for you to decide.
5. Builds one "golden" record per real contact, picking the best value for each field when
   members disagree.

### Input

```json
{
  "contacts": [
    { "name": "Jennifer Adams", "company": "Acme Corporation", "email": "jen@acme.com", "phone": "415-555-0100" },
    { "name": "Jen Adams", "company": "Acme Corp.", "email": "jen@acme.com", "phone": "" }
  ],
  "matchThreshold": 85,
  "reviewThreshold": 70
}
```

Only `contacts` is required. Everything else has a sensible default: 85/70 thresholds, "most
complete record wins" on conflicts, duplicates-to-review and a merge audit log both included.

### Output

One dataset, rows tagged by `type`:

| type | what it is |
|---|---|
| `contact` | The clean, deduplicated list — always emitted. One row per real contact, `_dedupGroupSize`/`_dedupMerged`/`_dedupConfidence` columns are metadata, safe to drop before re-importing. |
| `duplicateGroup` | A group of rows that got merged, shown for a sanity check. |
| `reviewPair` | A pair that scored too close to call — not merged, flagged for a human. |
| `mergeLog` | One entry per merged group: which fields agreed (`matchedOn`), which fields disagreed (`conflicts`, with every value seen and which one was kept), and how many records were folded in. |

The key-value store's `SUMMARY` reports `estimatedRecordsSaved` (rows eliminated),
`reductionPercent`, and `conflictsByField` — a count of how often each column disagreed across
merged groups, useful for spotting a chronically dirty field (e.g. phone numbers entered in ten
different formats).

### What it will NOT do

- It will not merge two different people who happen to share a company and surname (e.g. "John
  Smith" and "Jane Smith" at the same firm) — a shared surname or company alone never crosses
  the match threshold on its own.
- It does not call out to any external lookup or enrichment service — matching is based purely
  on the fields you give it. Garbage columns in means a weaker signal, not an error.
- It caps at 200,000 rows per run and skips oversized "blocks" (e.g. a thousand contacts sharing
  one generic `info@` domain) to avoid a runaway comparison count — those rows still get
  compared under every other field, just not against each other via that one key.
- `reviewThreshold` must not exceed `matchThreshold` — the run fails fast rather than merging
  everything.

### Who uses it

Sales ops cleaning a CRM before a migration · anyone who just merged two contact lists after an
acquisition · agencies handing back a de-duplicated lead list to a client · marketers about to
send an email blast who don't want the same person to get it three times.

# Actor input Schema

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

Your messy contact or company list, as an array of flat row objects (e.g. exported from a CRM as JSON). Combine freely with datasetId.

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

Optional Apify dataset ID to pull additional rows from (e.g. the output of a CRM export actor). Rows are appended to "contacts" before deduplication.

## `fields` (type: `array`):

Leave empty to auto-detect name/email/phone/company/address columns from your data. For manual control pass objects: {"field":"email","type":"email","weight":5}. Types: email, phone, person, company, address, numeric, text, exact.

## `matchThreshold` (type: `integer`):

Pairs scoring at or above this are merged automatically. Higher = safer (fewer wrong merges) but misses more real duplicates. Expressed 0-100; 85 means 0.85.

## `reviewThreshold` (type: `integer`):

Pairs scoring between this and the match threshold are reported for you to review instead of being merged automatically. Silently fusing two different people is worse than missing a duplicate.

## `survivorshipStrategy` (type: `string`):

When merged contacts disagree on a field (e.g. two different phone numbers), this decides which value survives into the clean record.

## `maxBlockSize` (type: `integer`):

Groups of records sharing a generic key (e.g. a common email domain) larger than this are skipped for comparison, to avoid a slowdown on huge lists.

## `includeDuplicatesOnly` (type: `boolean`):

Emit the merged duplicate groups plus any ambiguous pairs that scored too low to auto-merge, so you can sanity-check the run.

## `includeMergeLog` (type: `boolean`):

Emit one row per merged group explaining what was merged, which fields agreed, which fields conflicted, and which value was kept.

## Actor input object example

```json
{
  "contacts": [
    {
      "name": "Jennifer Adams",
      "company": "Acme Corporation",
      "email": "jen@acme.com",
      "phone": "415-555-0100"
    },
    {
      "name": "Jen Adams",
      "company": "Acme Corp.",
      "email": "jen@acme.com",
      "phone": ""
    },
    {
      "name": "J. Adams",
      "company": "ACME, Inc.",
      "email": "",
      "phone": "(415) 555 0100"
    },
    {
      "name": "Michael Chen",
      "company": "Globex Ltd",
      "email": "mchen@globex.io",
      "phone": "212-555-0199"
    },
    {
      "name": "Mike Chen",
      "company": "Globex Limited",
      "email": "mchen@globex.io",
      "phone": ""
    }
  ],
  "matchThreshold": 85,
  "reviewThreshold": 70,
  "survivorshipStrategy": "mostComplete",
  "maxBlockSize": 500,
  "includeDuplicatesOnly": true,
  "includeMergeLog": true
}
```

# 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": "Jennifer Adams",
            "company": "Acme Corporation",
            "email": "jen@acme.com",
            "phone": "415-555-0100"
        },
        {
            "name": "Jen Adams",
            "company": "Acme Corp.",
            "email": "jen@acme.com",
            "phone": ""
        },
        {
            "name": "J. Adams",
            "company": "ACME, Inc.",
            "email": "",
            "phone": "(415) 555 0100"
        },
        {
            "name": "Michael Chen",
            "company": "Globex Ltd",
            "email": "mchen@globex.io",
            "phone": "212-555-0199"
        },
        {
            "name": "Mike Chen",
            "company": "Globex Limited",
            "email": "mchen@globex.io",
            "phone": ""
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("that_red_bird/contact-deduplicator").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": "Jennifer Adams",
            "company": "Acme Corporation",
            "email": "jen@acme.com",
            "phone": "415-555-0100",
        },
        {
            "name": "Jen Adams",
            "company": "Acme Corp.",
            "email": "jen@acme.com",
            "phone": "",
        },
        {
            "name": "J. Adams",
            "company": "ACME, Inc.",
            "email": "",
            "phone": "(415) 555 0100",
        },
        {
            "name": "Michael Chen",
            "company": "Globex Ltd",
            "email": "mchen@globex.io",
            "phone": "212-555-0199",
        },
        {
            "name": "Mike Chen",
            "company": "Globex Limited",
            "email": "mchen@globex.io",
            "phone": "",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("that_red_bird/contact-deduplicator").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": "Jennifer Adams",
      "company": "Acme Corporation",
      "email": "jen@acme.com",
      "phone": "415-555-0100"
    },
    {
      "name": "Jen Adams",
      "company": "Acme Corp.",
      "email": "jen@acme.com",
      "phone": ""
    },
    {
      "name": "J. Adams",
      "company": "ACME, Inc.",
      "email": "",
      "phone": "(415) 555 0100"
    },
    {
      "name": "Michael Chen",
      "company": "Globex Ltd",
      "email": "mchen@globex.io",
      "phone": "212-555-0199"
    },
    {
      "name": "Mike Chen",
      "company": "Globex Limited",
      "email": "mchen@globex.io",
      "phone": ""
    }
  ]
}' |
apify call that_red_bird/contact-deduplicator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,that_red_bird/contact-deduplicator"
        }
    }
}

```

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/it8iJ9bFJU8Ic42Ne/builds/YwqoS2dASAFv19yff/openapi.json
