# How Reply detects duplicate contacts

> The deduplication rule — the contact identifier chosen at import, update-vs-skip handling for matched records, and LinkedIn URL normalization across every source.

**The contact identifier chosen at import — Email, Last Name, or Phone — becomes the distinguishing key that prevents duplicates; rows matching an existing record are updated or skipped, never doubled. LinkedIn URLs are normalized to a canonical form on every ingestion source, so URL variants of one profile do not create duplicates.**

_Status: Reviewed — human-edited, facts not yet confirmed against the product._

## At a glance

| Aspect | Detail |
| --- | --- |
| **What it governs** | Whether an incoming contact row creates a new record, updates an existing one, or is skipped as a duplicate |
| **Trigger** | A contact-creating operation: CSV import, API import, integrations, enrichment, CRM sync, or manual editing |
| **Inputs** | The contact identifier chosen at import (Email, Last Name, or Phone), the matched-records option, the LinkedIn profile URL |
| **Result** | Matches become updates or skips; import results report Imported, Updated, Skipped, and Import Errors counts |
| **Configurable** | The identifier field, Update Missing Fields vs. Skip for matched records, enrichment Keep-vs-Override, account import modes |
| **Does not** | Merge records that differ on the identifier, or normalize LinkedIn company-page URLs — those are stored as-is |
| **Plans** | Plan-specific differences are not documented for this rule |

## Rule summary

During a CSV import you choose the **contact identifier** in a drop-down menu. When you
choose a unique identifier — such as **Email, Last Name, or Phone** — it becomes the
**distinguishing key for each contact and prevents duplicates**; once chosen, the other
fields become optional. Rows that match an existing record are handled per your
matched-records option (update or skip) instead of creating a second record. Separately,
Reply normalizes every **LinkedIn profile URL** to one canonical format, so different URL
variants of the same profile are recognized as the same person.

## Trigger

Any contact-creating operation: a CSV import, a V3 API import, Website Visitors or RB2B
events, email-based or LinkedIn URL enrichment, a CRM integration sync, or manual editing
of the LinkedIn field in the contact card.

## Decision logic

1. Match the incoming row against existing contacts using the chosen identifier (Email, Last Name, or Phone).
2. On a match, apply the **Matched Records** option: **Update Missing Fields** writes new data from the CSV into the existing contact's fields, while **Skip** leaves the existing record untouched.
3. On no match, create a new contact.
4. Normalize the LinkedIn profile URL to the canonical form `https://www.linkedin.com/in/username` whenever a contact is created or updated. Duplicate detection by LinkedIn URL is **format-agnostic**: trailing slashes, query parameters, `http://` vs. `https://`, and a missing `www` are all recognized as the same profile and will not create duplicates.

**Import results report the outcome:**

| Import status | Meaning |
| --- | --- |
| Imported | New contacts added to the account |
| Updated | Existing contacts whose profiles were updated |
| Skipped | Duplicates the system removed during the import |
| Import Errors | Rows that failed — missing mandatory fields or incorrect formats (for example, symbols in the Phone field) |

## Result

One record per identifier value. Re-importing a list does not double contacts — matching
rows update or skip, and the notification after import breaks down exactly what happened.
If the reason is unclear, download the full import result file for details.

## State changes

| Field | Change |
| --- | --- |
| Matched contact | Fields updated per Update Missing Fields, or untouched per Skip |
| LinkedIn profile field | Stored in canonical `https://www.linkedin.com/in/username` form; automatic LinkedIn steps (Connect, Message) save the resolved profile URL back to the contact in the same form |
| Import summary | Imported / Updated / Skipped / Import Errors counts |

## Exceptions

- **Normalization is best-effort** — URLs that cannot be parsed, such as company pages (`linkedin.com/company/acme`) or a plain username without a URL, are stored as-is without an error.
- **CRM background sync historically skipped normalization** — this has been fixed, but contacts synced from a CRM (Salesforce, HubSpot, Pipedrive, Copper) before the fix may still hold non-normalized URLs until they are next updated.
- **RB2B leads arrive pre-normalized** — the LinkedIn URL received from RB2B is saved in the canonical format, so variants of the same profile from that source do not create duplicates either.
- **Two identifier values are two records** — deduplication keys on the chosen identifier, so a person appearing under two email addresses is not merged automatically.
- **Accounts have their own import modes** — including a deliberate **Duplicate** mode that creates a copy with the new data instead of touching the original.

## Configuration that changes this rule

At contact import: the identifier drop-down and the Matched Records choice (Update Missing
Fields vs. Skip). In Settings > Contact handling, the **enrichment settings** decide what
an update writes when new information is found: **Keep existing data** preserves current
information and skips conflicting new data; **Override with new data** replaces existing
fields. For account (company) imports from CSV, four modes exist: **Overwrite existing
fields**, **Update missing fields**, **Skip**, and **Duplicate** — with Account name
mandatory for mapping. See [Manage duplicate contacts](/how-to/manage-duplicates).

## Limits

| Limit | Value |
| --- | --- |
| Rows per contact CSV | Up to 10,000 |
| Rows per account CSV | Up to 10,000 |
| Identifier choices | A unique field such as Email, Last Name, or Phone |
| Canonical LinkedIn form | `https://www.linkedin.com/in/username` |
| Normalization coverage | CSV import, V3 API import, Website Visitors, RB2B, email-based enrichment, LinkedIn URL enrichment, CRM sync, manual edits, and automatic LinkedIn steps |

## FAQ

### What does "Skipped" mean in my import results?

Skipped rows are duplicates the system removed during the import — the identifier matched
an existing contact and your matched-records choice did not update it. Updated rows, by
contrast, are existing contacts whose profiles were changed. See
[Import contacts](/how-to/import-contacts).

### Do LinkedIn URL variants create duplicate contacts?

No. Duplicate detection by LinkedIn URL is format-agnostic — trailing slash, query
parameters, `http://` vs. `https://`, and missing `www` all resolve to the same canonical
`https://www.linkedin.com/in/username` form. The exception is unparseable values like
company-page URLs, which are stored as-is.

### What happens when the same person has two email addresses?

With Email as the identifier, each address is its own key, so the person gets two records
— automatic merging across different identifier values is not documented. Pick the record
whose address you actually contact, and see [Manage duplicate contacts](/how-to/manage-duplicates)
for cleanup.

### Are accounts deduplicated the same way as contacts?

Accounts imported from CSV offer four explicit modes — Overwrite existing fields, Update
missing fields, Skip, and Duplicate (which intentionally creates a copy while leaving the
original unchanged). Account name is the mandatory mapped field. See
[Companies](/specifications/companies).

## Related

- [Contacts](/specifications/contacts)
- [Contact enrollment](/behavior/contact-enrollment)
- [Manage duplicate contacts](/how-to/manage-duplicates)
- [Import contacts](/how-to/import-contacts)

## Build with Reply

- REST API: [docs.reply.io](https://docs.reply.io/api-reference/introduction) — create/update contact semantics
- MCP: [agents.reply.io/mcp](https://agents.reply.io/mcp)
