DUPLICATES_DETECTED: Use one of these records?
Salesforce's Duplicate Rules engine flagged your record as a fuzzy match for an existing one. Different from `DUPLICATE_VALUE`, which is a hard unique-field constraint. The fix is either to skip the duplicate, merge with the existing record, or pass `allowSave=true` if you legitimately want to keep both.
Also seen asDUPLICATES_DETECTED·Use one of these records·duplicates detected api·duplicate rule blocked save
A marketing automation platform pushes a list of 5,000 leads to Salesforce via the REST API every morning. The first run after a quarterly business review goes wrong: 1,200 of the 5,000 inserts return DUPLICATES_DETECTED: Use one of these records?. The integration logs show the error response, but the records were not created and nothing tells the operations team which existing records the API thought were duplicates.
What the platform is checking
Salesforce Duplicate Management enforces matching and duplicate rules at save time. A matching rule defines what makes two records "the same" for a given object: same email, same name plus same phone, fuzzy match on company name. A duplicate rule references a matching rule and decides what to do when a match is found: block the save, allow it with a warning, or report it.
When an API call attempts to insert or update a record that matches an existing record under an active duplicate rule with the Block action, the platform refuses the save and returns DUPLICATES_DETECTED. The response includes the ids of the matched existing records. The client can then decide whether to update one of them, merge, or override the rule.
The rule operates server-side and applies uniformly to every save path: UI, API, Data Loader, Apex DML, Flow. A rule that blocks duplicates in the UI also blocks them through every other channel. This is intentional: the duplicate rule is a data-quality contract, not a UI annoyance.
The API error is precise about what happened (duplicates detected) and includes the matched ids in the response. It is less precise about which fields matched. The matching rule that fired is the source of truth, but the API response by default does not name the rule.
What actually matches
Three common matching patterns produce most duplicate detections.
Email-based matching on Lead and Contact. The default Standard Lead Matching Rule and Standard Contact Matching Rule both key on Email. Any Lead or Contact insert with an email that already exists in the org matches an existing record. The match works across both Lead and Contact in cross-object rules.
Fuzzy name and company matching. Custom matching rules can use Levenshtein distance or normalized string comparison. "Acme Corp" and "Acme Corporation" match. "John Smith" and "Jon Smith" match. The fuzzy logic catches many real duplicates and also some legitimately distinct records.
Composite key matching. A custom rule that combines multiple fields (Name + Phone + ZIP) matches when all the listed fields agree. Composite rules catch household duplicates and other cases where any single field is not unique.
The broken example
An integration that inserts Leads from a CSV via the REST API:
async function loadLeads(leads) {
for (const lead of leads) {
const res = await fetch('/services/data/v60.0/sobjects/Lead', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(lead)
});
if (!res.ok) {
const body = await res.json();
console.error('Failed:', body);
}
}
}
The integration loops record by record. For each Lead that matches an existing record under the active duplicate rule, the response is a 400 with body:
[{
"message": "Use one of these records?",
"errorCode": "DUPLICATES_DETECTED",
"duplicateResut": {
"matchResults": [{
"matchEngine": "ExactMatchEngine",
"rule": "Standard Lead Matching Rule v1.0",
"matchRecords": [{
"record": { "Id": "00Q...", "Name": "Jane Doe", "Email": "jane@acme.com" }
}]
}]
}
}]
The integration logs the error and moves on. The 1,200 duplicate Leads are dropped from the batch. The marketing team sees that fewer Leads landed than they sent and has no easy way to see which existing records the duplicates collided with.
The fix, three paths
Update the matched record instead of inserting. When a duplicate is detected, the response includes the matched record's id. The integration can switch to an update against that id, merging the new data with the existing record.
async function loadLeads(leads) {
for (const lead of leads) {
const res = await fetch('/services/data/v60.0/sobjects/Lead', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(lead)
});
if (res.status === 400) {
const body = await res.json();
const dup = body[0]?.duplicateResut?.matchResults?.[0]?.matchRecords?.[0]?.record;
if (dup?.Id) {
await fetch(`/services/data/v60.0/sobjects/Lead/${dup.Id}`, {
method: 'PATCH',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(lead)
});
}
}
}
}
The integration becomes an upsert that respects the matching rule. New Leads insert. Matched Leads update the existing record. Nothing is dropped.
Use the Composite API or upsert with an external id. Salesforce supports upsert against an external-id field. If the source system has its own unique id (Marketing_Cloud_Id__c, External_Id__c), the integration can upsert against that field. The match happens server-side against the external id, not against the email or name. Duplicate rules can still fire if the external id maps to a record that violates the rule, but the common case (re-inserting the same Lead) becomes a clean update.
await fetch(`/services/data/v60.0/sobjects/Lead/External_Id__c/${lead.External_Id__c}`, {
method: 'PATCH',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify(lead)
});
The HTTP method is PATCH on a URL that includes the external-id field name and value. Salesforce upserts: insert if no matching external id exists, update if one does.
Override the duplicate rule for this save. Some integrations have business reasons to bypass the rule (a controlled bulk import where duplicates are accepted, a migration where the source-of-truth treats each row as canonical). The Allow Save with Save Options header overrides the rule for a single API call:
await fetch('/services/data/v60.0/sobjects/Lead', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
'Sforce-Duplicate-Rule-Header': 'allowSave=true'
},
body: JSON.stringify(lead)
});
The override applies only to the specific call. The duplicate rule remains active for every other save path. Use this sparingly; bypassing duplicate rules in routine integrations defeats the purpose of having them.
The fixed example
A Node.js integration that handles duplicates by updating, falling back to insert:
async function upsertLead(lead, token) {
const externalId = lead.External_Id__c;
if (!externalId) {
return insertNew(lead, token);
}
const res = await fetch(
`/services/data/v60.0/sobjects/Lead/External_Id__c/${encodeURIComponent(externalId)}`,
{
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(lead)
}
);
if (res.ok || res.status === 201 || res.status === 204) {
return { status: 'upserted', id: (await res.json()).id };
}
const body = await res.json();
if (body[0]?.errorCode === 'DUPLICATES_DETECTED') {
const dupId = body[0].duplicateResut?.matchResults?.[0]?.matchRecords?.[0]?.record?.Id;
if (dupId) {
await fetch(`/services/data/v60.0/sobjects/Lead/${dupId}`, {
method: 'PATCH',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ ...lead, External_Id__c: externalId })
});
return { status: 'merged', id: dupId };
}
}
return { status: 'error', error: body };
}
The integration first attempts an upsert by external id. On success, the record is created or updated. On a duplicate detection (which happens when the external id is new but the email matches an existing record), the code captures the matched id and updates that record, also stamping the external id so future runs can upsert directly.
Edge cases and gotchas
Cross-object matching. A Lead can match a Contact under cross-object rules. The API response identifies the matched record by its object type (Contact, Account) even though the insert was on Lead. The integration must handle the case where the matched object is not the one being inserted.
Soft-deleted records. A duplicate match against a record in the Recycle Bin counts as a match. The save is blocked even though the record is "deleted". Empty the Recycle Bin or change the matching rule to exclude deleted records.
Inactive matching rules and stale cache. A matching rule that was deactivated five minutes ago can still produce matches because the platform caches rule definitions for a short window. After deactivation, retry the integration a few minutes later if the matching rule is no longer expected.
Bulk API behavior. The Bulk API treats duplicate detection slightly differently. Each row is processed independently; one duplicate does not abort the batch. The result CSV identifies which rows succeeded and which were blocked. Bulk integrations should parse the results CSV and re-process the blocked rows.
Field-level differences. A matching rule that uses standard fields (Email, Phone) but ignores custom fields can flag two records as duplicates that have completely different business attributes. The "duplicate" detection is based on the rule, not on full record equality. Review the rule periodically to make sure it captures the intended notion of duplication.
Activity history and related lists. Updating the matched record rather than inserting a new one preserves the existing record's activity history, related lists, and audit trail. This is usually desired but worth confirming with the business owners.
Defensive habits
Treat every integration that creates records as an upsert candidate. Bare inserts assume the source-of-truth and the target are perfectly synchronized, which is rarely the case. Upserts handle the realistic state where some records exist and some do not.
Maintain external-id fields on every object the integration touches. The integration's internal id stamped on the Salesforce record provides a stable matching key that is faster and more reliable than fuzzy matching on business fields.
Log the duplicate detection events. A Duplicate_Audit__c custom object that records the source row, the matched record, and the action taken (insert, update, skip) gives the operations team a record of what the integration decided when it could have gone two ways.
Review duplicate rule configuration periodically. A rule that was useful three years ago may now be too aggressive (blocking legitimate distinct records) or too permissive (missing actual duplicates). Quarterly review of rule effectiveness keeps the configuration current.
For migrations and one-time bulk loads, consider deactivating duplicate rules during the load and reactivating after, then running a Duplicate Job to surface duplicates that slipped through. The Duplicate Job is server-side, fast, and produces a Duplicate Record Set for each cluster of matches.
Test patterns
Apex test for a save that should respect a duplicate rule:
@IsTest
static void insertingDuplicateLeadIsBlocked() {
Lead existing = new Lead(
FirstName = 'Jane',
LastName = 'Doe',
Email = 'jane@acme.com',
Company = 'Acme'
);
insert existing;
Lead duplicate = new Lead(
FirstName = 'Jane',
LastName = 'Doe',
Email = 'jane@acme.com',
Company = 'Acme'
);
Test.startTest();
Database.SaveResult result = Database.insert(duplicate, false);
Test.stopTest();
System.assertEquals(false, result.isSuccess(), 'Should be blocked');
Database.Error err = result.getErrors()[0];
System.assertEquals('DUPLICATES_DETECTED', String.valueOf(err.getStatusCode()));
}
The test relies on the Standard Lead Matching Rule being active. Tests for custom rules should explicitly set up the matching scenario with the relevant fields populated.
Diagnosing in production
When the API returns DUPLICATES_DETECTED:
- Parse the response body and capture the matched record id.
- Compare the inbound record against the matched record. Are they actually the same business entity?
- If yes, decide whether to update the existing record or merge the inbound data manually.
- If no, the matching rule is overmatching. Review the rule and tighten the criteria.
- Add an entry to the duplicate audit log so the pattern is visible across runs.
The matched record id is the most actionable piece of the response. Every well-built integration captures it and acts on it.
Quick recovery checklist
- Switch the integration from insert to upsert against an external id.
- Handle the DUPLICATES_DETECTED response with a follow-up update.
- Log the decision so the data team can audit.
- Review the matching rule if duplicates flag too aggressively.
- Confirm the runbook covers the scenarios end-to-end.
Duplicate detection is one of the most useful features in Salesforce when configured well. The integration design that respects it is also one of the most data-clean integration patterns available.
Further reading from Salesforce
Related dictionary terms
Share this fix
Related Integration errors
API_DISABLED_FOR_ORG: API is not enabled for this Organization or Partner
IntegrationThe org's edition or the user's profile doesn't include API access. Most often this is a Professional Edition org without the API add-on, or…
ChangeDataCapture: missing ChangeEventHeader / cannot replay events
IntegrationYour Change Data Capture subscriber isn't receiving events, or events are arriving without a `ChangeEventHeader`. Usually means CDC isn't en…
EXCEEDED_ID_LIMIT: record limit reached
IntegrationYou hit one of Salesforce's hard caps on a record-related operation: too many sharing rows on one record, too many child records on one pare…
Failed to load batch — InvalidBatch: invalid CSV header / unrecognized field
IntegrationYour Bulk API job's CSV has a column the target object doesn't have, or the header row is malformed. The job's `failedResults` lists the bad…
INVALID_SESSION_ID: Session expired or invalid
IntegrationYour API call presented a session token Salesforce no longer accepts — it timed out, was logged out from elsewhere, was issued by a differen…