Deduplication
Every POST /leads request runs automatic duplicate detection before inserting. This prevents polluting your database with duplicate entries from different sources.
Detection priority
Section titled “Detection priority”| Priority | Criterion | Confidence | Action |
|---|---|---|---|
| 1 | email match (case-insensitive) | High | Merge into existing |
| 2 | source_id exact match | High | Merge into existing |
| 3 | phone match + name similarity > 80% | Medium | Merge into existing (similarity uses normalized string comparison — “Acme Corp” and “ACME Corporation” may not match) |
| 4 | name + city match (case-insensitive) | Low | Create new, flag with potential_duplicate_id |
Merge strategy: Last Touch Wins
Section titled “Merge strategy: Last Touch Wins”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_atis always bumped — even if no field changed.
This models “freshness of interest”: every new push from a source registers as a new interaction.
Response on duplicate
Section titled “Response on duplicate”{ "id": "cl_existing...", "message": "Duplicate detected, existing lead updated", "created_at": 1703520000, "duplicate": true}HTTP status is 200 OK (not 201) on a merge.
Potential duplicate (low confidence)
Section titled “Potential duplicate (low confidence)”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.
updated_at vs last_interaction_at
Section titled “updated_at vs last_interaction_at”| Field | Updates when |
|---|---|
updated_at | A structural field is modified (name, email, city, etc.) |
last_interaction_at | Any 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.
Audit trail
Section titled “Audit trail”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", "reason": "dedup:email", "created_at": 1703520000 } ], "count": 1}action | When |
|---|---|
created | New lead inserted |
merged | Dedup merge into an existing lead (reason is dedup:email, dedup:source_id, dedup:phone+name, or dedup:name+city) |
updated | Explicit PUT /leads/{id} (or batch update) |
deleted | Lead 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.