Skip to content

Deduplication

Every POST /leads request runs automatic duplicate detection before inserting. This prevents polluting your database with duplicate entries from different sources.

PriorityCriterionConfidenceAction
1email match (case-insensitive)HighMerge into existing
2source_id exact matchHighMerge into existing
3phone match + name similarity > 80%MediumMerge into existing (similarity uses normalized string comparison — “Acme Corp” and “ACME Corporation” may not match)
4name + city match (case-insensitive)LowCreate new, flag with potential_duplicate_id

When a duplicate is detected at priority 1–3:

  • Non-empty fields from the incoming request overwrite the existing lead.
  • Fields not present in the request are left unchanged.
  • last_interaction_at is always bumped — even if no field changed.

This models “freshness of interest”: every new push from a source registers as a new interaction.

{
"id": "cl_existing...",
"message": "Duplicate detected, existing lead updated",
"created_at": 1703520000,
"duplicate": true
}

HTTP status is 200 OK (not 201) on a merge.

On a name+city match only, a new lead is created but the response includes a hint:

{
"id": "cl_new...",
"potential_duplicate_id": "cl_similar...",
"message": "Lead created successfully"
}

You can review these in the dashboard: navigate to Leads, search for the potential_duplicate_id, and manually merge or dismiss.

FieldUpdates when
updated_atA structural field is modified (name, email, city, etc.)
last_interaction_atAny push arrives — including merges with no field changes

Use sort_by=last_interaction_at&sort_order=DESC to find the leads with the most recent activity, independent of data changes.

Every create, merge, update, and delete is recorded. Fetch the history for a lead:

GET /leads/{id}/changes?limit=50
{
"changes": [
{
"id": "uuid-...",
"lead_id": "cl_...",
"user_id": "owner_user_id",
"actor_id": "actor_user_id",
"action": "merged",
"before": { "name": "Acme", "email": "[email protected]" },
"after": { "name": "Acme", "email": "[email protected]" },
"reason": "dedup:email",
"created_at": 1703520000
}
],
"count": 1
}
actionWhen
createdNew lead inserted
mergedDedup merge into an existing lead (reason is dedup:email, dedup:source_id, dedup:phone+name, or dedup:name+city)
updatedExplicit PUT /leads/{id} (or batch update)
deletedLead removed

before / after are JSON snapshots of the lead. Default limit is 50 (max 100). History remains available even after the lead row is deleted.