Duplicate leads are an intake problem that shows up as a data problem. Two reps call the same person, a report counts one buyer twice, and the second record carries a later campaign that takes credit for the first. Salesforce's duplicate rules help, but they were designed around people typing records in, and most leads today arrive from forms, booking tools and integrations. This guide starts with the check that runs before a lead exists, using our own org as the worked example, then covers the rules, phone matching and merging.
Where do duplicate leads come from?
| Source | How the duplicate appears | Guard |
|---|---|---|
| Repeat form fills | The same person comes back weeks later and becomes a second lead with a new first touch | Match email at intake; log the repeat as a Task |
| Retries from forms, webhooks and booking tools | One submission delivered twice within seconds | An idempotency key: a request ID, a booking ID, or an External ID upsert |
| Web-to-Lead | Every submit creates a lead, with no check in front | Send forms through an intake step that checks first |
| Product signups synced from an app | A new lead per sync run when the record is not keyed | Upsert on an External ID such as the app's user ID |
| Phone and voice intake | The same caller with the number formatted differently | Normalize to digits and match on every phone field |
| Imports and event lists | People who already exist, loaded again | Match before the load; expect rules to block, not warn |
| Two brands in one org | The same email under each brand, matched to the wrong one | Scope the match by record type or brand |
Should you block, alert, or match and log?
There are four ways to respond to a likely duplicate, and they suit different entry points. Salesforce's duplicate rules offer the first three; the fourth is logic you put in front of the insert.
| Response | What happens | Use it for | The cost |
|---|---|---|---|
| Block (duplicate rule) | The save fails with a duplicate error | Manual entry where a duplicate is always a mistake | An integration that does not catch the error loses the lead |
| Alert (duplicate rule) | The user sees a warning and can save anyway | Manual entry where a person should decide | Through the API and import tools it acts as a block |
| Report (duplicate rule) | The record saves and joins a duplicate record set | Measuring the problem before you enforce anything | Somebody has to work the sets, or nothing changes |
| Match and log (intake logic) | No new lead; the submission is logged on the existing record | Forms, chat, bookings, product signups | You build and own it, and the repeat touch is free text unless you design for it |
How does our intake handle a repeat form fill?
Every website inquiry to our own Salesforce org goes through one n8n workflow, and its first Salesforce step is a lookup, not an insert. We chose that over Web-to-Lead because Web-to-Lead is form-only, creates a lead on every submission and gives the site no reliable success signal. The workflow:
- Looks for the newest unconverted Lead with the exact email (query below), with the email escaped before it goes into SOQL.
- When it finds one, it creates a completed Task on that Lead with the subject “Website form:” plus the topic, puts the new message and the latest click detail in the description, and sends a repeat-submission notice. It updates no field on the Lead and adds no Campaign member.
- When it finds none, it creates the Lead with the full attribution block, and a flow adds the Campaign member from the campaign key.
Lead activity timeline
Day 1
Lead created from the website form
Lead Source and the UTM fields are written once, from the first visit
Day 1
Campaign member added
Status Form Submitted, matched on the campaign key
Day 23
Task, completed: Website form: pricing question
Holds the new message and the latest click detail. A repeat-submission notice goes to the owner
Day 23
First-touch fields unchanged
No second lead, no overwrite, no second Campaign member
-- Our intake today: newest unconverted Lead with this email
SELECT Id, Email, Name FROM Lead
WHERE Email = :email AND IsConverted = false
ORDER BY CreatedDate DESC LIMIT 1
-- What we add on client builds: scope to the brand, then check Contacts
SELECT Id, OwnerId FROM Lead
WHERE Email = :email AND IsConverted = false
AND RecordType.DeveloperName = :brandRecordType
ORDER BY CreatedDate DESC LIMIT 1
SELECT Id, AccountId, OwnerId FROM Contact
WHERE Email = :email
ORDER BY LastModifiedDate DESC LIMIT 1Two other entry points follow the same idea with a different key. Bookings come from Cal.com's own Salesforce app, which writes the booking UID on the Event, and a check on that UID stops Cal.com's retries from creating a second Event. Product signups from our ApexGenius app are upserted on an External ID holding the app's user ID; a signup whose email matches an unconverted lead with no user ID adopts that lead instead of creating a new one. The cutover check for that sync is the aggregate query further down, scoped to the product's record type and expected to return zero rows.
Here is what it does not do yet. It matches Leads only, so a person who already converted to a Contact arrives as a new Lead. It does not filter by record type, so a lead for our consulting brand and one for our marketing brand with the same email can collide. And the repeat touch lives in Task text, which a person can read but a report cannot group by campaign. The second and third queries above are what we add on client builds. The reporting trade-off is covered in how we tie every ad lead to a Salesforce Campaign.
How do Salesforce duplicate and matching rules treat leads?
A duplicate rule decides what happens (block, alert, report), and a matching rule decides what counts as a match. The standard lead matching rule is active by default, and its equation is the first thing to read, because every branch of it needs both First Name and Last Name:
- First Name AND Last Name AND Email
- First Name AND Last Name AND Phone (alone, or with Company)
- First Name AND Last Name AND Title AND Company (or Email)
- First Name AND Last Name AND Mailing Street AND (City, ZIP, Phone or Title)
Email is compared exactly, first names fuzzily (exact, initials, Jaro-Winkler, Metaphone 3 and name variants, threshold 85), and phone by section, which Salesforce says works best with North American data (Standard Contact Matching Rule and Standard Lead Matching Rule, Salesforce Help, read 16 October 2026). A blank name field is ignored when Email is part of the comparison; a different one is not. The practical consequence: an intake that fills a missing last name with a placeholder, or splits a single name field differently from the way a rep types it, produces leads the standard rule will not match even when the email is identical. If email is your identity key, add a custom lead matching rule with Email on an exact match. Salesforce documents the same fix for accounts that share only a name (Knowledge Article 000386375).
| Situation | What Salesforce does |
|---|---|
| Lead created through an API or the data import tools | No alert is shown and the record cannot be saved, so an Alert rule acts as a Block |
| Several records saved at once (a batch or an import) | Block and Alert compare them with existing records only, not with each other; Report does include them |
| A record is edited | The rule runs only when an edited field is in the matching rule |
| The user cannot see a field the matching rule uses | The rule does not find duplicates for that user |
| Quick Create, undelete, manual merge, or lead convert without Apex Lead Convert | Duplicate rules do not run |
| A Workflow Rule or Process Builder field update | Duplicate rules do not run on that update |
| Rule limits | Up to five active duplicate rules per object and up to three matching rules in each |
Sources, read 16 October 2026: Things to Know About Duplicate Rules and Knowledge Article 000386375. For an integration that should save past an Alert rule, REST API accepts Sforce-Duplicate-Rule-Header: allowSave=true from API version 52.0 (Duplicate Rule header); Apex has the equivalent in Database.DMLOptions.DuplicateRuleHeader.
How do you catch a duplicate error in a flow?
A mass-tort intake firm whose org we worked in creates a person record from a completed intake questionnaire in a flow. When the duplicate rule stops the insert, the flow's fault path does not end the run:
- A decision checks whether the fault message contains
DUPLICATES_DETECTEDor “Duplicate Alert”. - If it does, the flow gets the most recent existing record with the same first name, last name and phone.
- It links the intake to that record and continues, so the case is never left without a person attached.
- Any other fault ends the run on the default outcome. That is the branch we would add an error log and an alert to, because only the duplicate case has a safe automatic answer and the rest need a person.
The same org also had a scheduled job meant to flag likely duplicate intakes from duplicate record sets. In the snapshot we reviewed, the duplicate rule feeding those sets was inactive, so the job ran and had nothing to flag. When any automation depends on a duplicate rule, add a check that the rule is still active to whatever you already monitor.
How do you match leads by phone number?
Phone numbers arrive as (954) 555-0100, +1 954-555-0100 and 9545550100, and an exact match treats those as three people. Normalize first, store the normalized value in a text field beside each phone field, and match on those fields.
// Normalize before matching. Store the result in a text field
// next to each phone field and match on those, not on raw input.
public static String phoneKey(String raw) {
if (String.isBlank(raw)) return null;
String digits = raw.replaceAll('[^0-9]', '');
// North American numbers: drop a leading country code 1.
if (digits.length() == 11 && digits.startsWith('1')) {
digits = digits.substring(1);
}
return digits.length() >= 10 ? digits : null;
}A behavioral health provider whose org we worked in takes calls through an AI voice agent that posts each call to an Apex REST endpoint. Numbers are reduced to digits, and a flow looks for a lead whose normalized phone, mobile or contact phone matches, then updates that lead or creates one. That keeps one lead per caller. The update path also writes the latest call's click ID and campaign over the existing values, so the lead ends up credited to the most recent call instead of the first one. If first touch matters to you, write attribution fields only when they are blank. The voice intake itself is in a voice AI answers the phone, Salesforce gets a perfect lead.
How do you merge duplicate leads without losing data?
In Lightning Experience you open a lead, click View Duplicates, choose up to three leads, pick one as the principal and choose the field values to keep. You need Delete on leads to merge. After the merge (Merge Duplicate Leads in Lightning Experience and Things to Know About Merging Duplicate Leads, read 16 October 2026):
- The non-principal leads move to the Recycle Bin.
- Related items such as campaign memberships and activities move to the merged lead, with some exceptions, and each campaign's Member Status Updated date is kept.
- Chatter feeds come from the principal record only; files are kept.
- The merged lead keeps the Created By and Created Date of the oldest lead, whichever one you chose as principal.
- Hidden and read-only fields keep the principal record's values.
Apex merge follows the same limit of three records of one object, deletes the duplicates and reparents related records (Merging Records, Apex Developer Guide). The risk in code is the field copy you write before the merge. On a 2023 project, Apex we reviewed found duplicate contacts with Datacloud.FindDuplicates, copied every field from the duplicate onto the survivor, blanks included, and queried inside a loop. The blanks erased good values on the survivor. Decide the survivor rules before anyone merges anything:
| Field group | Keep |
|---|---|
| First-touch attribution (UTM values, click IDs, landing page, Lead Source) | The oldest lead's values |
| Email and phone | The most recently confirmed value that is not blank |
| Owner | The rep with the open activity or the most recent contact |
| Status | The most advanced status |
| Consent and opt-outs | The most restrictive choice on any of the records |
| Any field | A value over a blank, never a blank over a value |
How do you find the duplicate leads you already have?
An aggregate query lists every email held by more than one open lead. A version of it scoped to one record type is the check we run after a sync cutover, where the expected result is zero rows.
SELECT Email, COUNT(Id) n
FROM Lead
WHERE IsConverted = false AND Email != null
GROUP BY Email
HAVING COUNT(Id) > 1Add a record type filter if the org holds more than one brand, and run the same query on Contact. For an ongoing view, set the duplicate rule to Report for a few weeks before switching it to Block or Alert: the duplicate record sets show how many duplicates each entry point creates, which tells you where the intake check belongs. If a clinical record is involved, the same principle (check before you create, on a key you trust) is what stops duplicate charts in our AdvancedMD and Salesforce integration. Who owns the lead once it is single is the subject of Salesforce lead routing architecture.
Frequently asked questions
How do I prevent duplicate leads in Salesforce?
Check before you create. Route every form, chat, booking and integration through one intake step that looks for an existing unconverted lead or contact with the same email (and phone, for call intake) and logs the new submission on that record when it finds one. Then add duplicate rules as a second net for manual entry and imports.
Why doesn't my Salesforce duplicate rule catch leads with the same email?
The standard lead matching rule never matches on email alone. Every branch of its equation also compares First Name and Last Name, so the same email under a different last name, such as a placeholder an intake filled in, is not a duplicate to it. Create a custom lead matching rule with Email on an exact match and add it to your duplicate rule.
Do duplicate rules block leads created through the API?
Yes. When records are added through Salesforce APIs or the data import tools, no alert is shown and the record cannot be saved, so an Alert rule behaves like a Block and the API returns DUPLICATES_DETECTED. An integration can send the Sforce-Duplicate-Rule-Header with allowSave=true (REST, API 52.0 and later) to save anyway.
How many leads can you merge at once in Salesforce?
Three. In Lightning Experience you choose up to three leads, pick one as the principal record and choose which field values to keep; the other leads go to the Recycle Bin. Apex merge has the same limit of three records of one object per merge.
Should a repeat form fill update the existing lead?
Not the attribution fields. Overwriting UTM values, click IDs or landing page on a repeat visit erases the first touch that brought the lead in. Log the new submission as an activity on the existing lead, and if repeat touches by campaign must be reportable, add a second Campaign member on purpose.