Duplicates
Detect and merge duplicate records. Scan for matches by email, phone, LinkedIn, or name and consolidate them into a single master record.
The duplicates endpoint provides two operations: detecting potential duplicate records and merging confirmed duplicates. Both operations require admin privileges.
Both actions on this endpoint are restricted to admin users. Non-admin tokens receive a 403 error.
Detect Duplicates
POST /api/v1/duplicatesScans records for potential duplicates based on matching criteria.
Request Body
{
"action": "detect",
"object_name": "contacts",
"fields": ["email", "phone"],
"limit": 100
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
action | string | Yes | Must be "detect" | |
object_name | string | Yes | The object to scan (e.g. contacts, companies) | |
fields | string[] | No | all match fields | Restrict matching to specific fields. Accepted values: email, phone, linkedin, name. |
limit | integer | No | 50 | Maximum number of duplicate groups to return |
Matching Rules
The detection engine scans up to 5000 records and matches on the following criteria (all comparisons are exact, case-insensitive):
| Match Field | Description |
|---|---|
email | Matches on any email in the record's email fields |
phone | Matches on normalized phone numbers |
linkedin | Matches on LinkedIn profile URL or vanity name |
name | Matches on first name + last name (or company name) |
Example Request
curl -X POST "https://kasar.app/api/v1/duplicates" \
-H "Authorization: Bearer ksr_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"action": "detect",
"object_name": "contacts",
"fields": ["email", "name"],
"limit": 50
}'Response
{
"duplicate_groups": [
{
"records": [
{
"id": "550e8400-...",
"first_name": "Alice",
"last_name": "Martin",
"email": "alice@example.com",
"created_at": "2025-01-15T10:30:00Z"
},
{
"id": "660f9511-...",
"first_name": "Alice",
"last_name": "Martin",
"email": "alice@example.com",
"created_at": "2025-02-20T14:00:00Z"
}
],
"match_reason": "email"
}
],
"total_groups": 1,
"records_scanned": 3420
}| Field | Type | Description |
|---|---|---|
duplicate_groups | object[] | Array of groups, each containing matching records |
duplicate_groups[].records | object[] | The records that match each other |
duplicate_groups[].match_reason | string | The field that triggered the match (e.g. email, phone, name) |
total_groups | integer | Total number of duplicate groups found |
records_scanned | integer | Number of records that were scanned |
Merge Duplicates
POST /api/v1/duplicatesMerges one or more duplicate records into a master record. All relations (BELONGS_TO_ONE foreign keys and MANY_TO_MANY junction entries) are re-linked to the master record before the duplicates are deleted.
Request Body
{
"action": "merge",
"object_name": "contacts",
"master_id": "550e8400-...",
"duplicate_ids": ["660f9511-..."],
"merge_strategy": "fill_blanks"
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
action | string | Yes | Must be "merge" | |
object_name | string | Yes | The object containing the records | |
master_id | UUID | Yes | The record to keep as the canonical version | |
duplicate_ids | UUID[] | Yes | Records to merge into the master and delete | |
merge_strategy | string | No | keep_master | How to handle conflicting field values |
Merge Strategies
| Strategy | Description |
|---|---|
keep_master | Keep all field values from the master record. Duplicate values are discarded. This is the default. |
fill_blanks | For each field, if the master has a blank/null value, fill it with the value from the duplicate. Non-blank master fields are preserved. |
Example Request
curl -X POST "https://kasar.app/api/v1/duplicates" \
-H "Authorization: Bearer ksr_a1b2c3d4e5f6..." \
-H "Content-Type: application/json" \
-d '{
"action": "merge",
"object_name": "contacts",
"master_id": "550e8400-e29b-41d4-a716-446655440000",
"duplicate_ids": ["660f9511-f3ac-52e5-b827-557766551111"],
"merge_strategy": "fill_blanks"
}'Response
{
"master_id": "550e8400-e29b-41d4-a716-446655440000",
"duplicates_deleted": 1,
"strategy": "fill_blanks"
}| Field | Type | Description |
|---|---|---|
master_id | UUID | The ID of the surviving master record |
duplicates_deleted | integer | Number of duplicate records that were deleted |
strategy | string | The merge strategy that was applied |
What Happens During a Merge
- Field values are resolved according to the chosen strategy.
- BELONGS_TO_ONE relations: Any records pointing to a duplicate via a foreign key are updated to point to the master instead.
- MANY_TO_MANY junctions: Junction table entries referencing duplicates are re-linked to the master. If a junction already exists for the master, the duplicate entry is removed to avoid conflicts.
- Duplicate records are permanently deleted.
The merge operation is atomic. If any step fails, the entire operation is rolled back and no data is modified.
Error Responses
| Status | Code | Description |
|---|---|---|
| 400 | INVALID_OBJECT | The object name does not exist |
| 400 | VALIDATION_ERROR | Invalid action, missing required fields, or master_id appears in duplicate_ids |
| 403 | PERMISSION_DENIED | Token does not belong to an admin user |
| 404 | RECORD_NOT_FOUND | The master record or one of the duplicate records was not found |