Contact management API returns duplicate records on search queries

After importing 50,000 new contacts from our marketing automation system last week, we’re seeing duplicate records returned by the contact search API. When we search by email or company name, the same contact appears multiple times in the results with different IDs but identical data.

Example query:


GET /api/contacts/search?email=john.doe@example.com
Response: [{id: 12345, email: "john.doe@example.com", ...},
           {id: 67890, email: "john.doe@example.com", ...}]

The UI shows only one contact record when we search manually, so the duplicates seem to be API-specific. We’ve checked our import process and confirmed we’re not sending duplicate records. The issue is affecting our sales team’s lead assignment automation and causing confusion about which contact ID is the “correct” one to use. Is this a known issue with the search API after large imports, or is there a query parameter we need to use for deduplication?

I’ll provide a comprehensive solution addressing data import deduplication, API query filters, and backend data consistency issues.

Understanding the Duplicate Records Issue: The duplicate records appearing in API search results but not in the UI indicates a disconnect between three components:

  1. Primary database (where actual contact records are stored)
  2. Search index (used by the API for fast queries)
  3. UI query layer (which may apply additional deduplication logic)

Your bulk import likely created true duplicate records in the database that the UI is filtering out client-side, while the API returns raw search index results.

Data Import Deduplication:

Step 1: Verify Actual Duplicates in Database First, confirm whether duplicates exist in the primary database or just the search index:

GET /api/admin/contacts/duplicates?field=email&threshold=exact

Response:
{
  "duplicate_groups": [
    {
      "email": "john.doe@example.com",
      "record_ids": [12345, 67890],
      "created_dates": ["2025-09-25", "2025-09-25"]
    }
  ],
  "total_duplicates": 1247
}

This confirms true duplicates exist in the database.

Step 2: Analyze Import Job for Root Cause Review your import job configuration and logs:

GET /api/admin/imports/job-12345/details

Response:
{
  "job_id": "job-12345",
  "records_processed": 50000,
  "records_inserted": 51247,  // More than source!
  "duplicates_detected": 0,
  "deduplication_enabled": false,  // Root cause
  "match_fields": []
}

The issue: deduplication wasn’t enabled during import, so all 50,000 records were inserted even if some matched existing contacts.

Step 3: Configure Proper Deduplication Rules For future imports, enable deduplication with appropriate match fields:

POST /api/admin/imports/configure
{
  "deduplication": {
    "enabled": true,
    "match_fields": ["email"],
    "match_strategy": "exact",
    "on_duplicate": "update_existing",
    "preserve_master_record": "oldest"
  }
}

This ensures future imports update existing contacts rather than creating duplicates.

API Query Filters for Deduplication:

While you fix the backend data, you can use API filters to work around the duplicate results:

Option 1: Use Distinct Query Parameter

GET /api/contacts/search?email=john.doe@example.com&distinct=true&distinct_field=email

This returns only one record per unique email address.

Option 2: Filter by Master Record Flag

GET /api/contacts/search?email=john.doe@example.com&master_only=true

This returns only records marked as master (not duplicates).

Option 3: Client-Side Deduplication If API parameters aren’t available, deduplicate in your code:

response = GET("/api/contacts/search?email=john.doe@example.com")
contacts = response.json()["contacts"]

# Deduplicate by email, keep oldest record
unique_contacts = {}
for contact in contacts:
    email = contact["email"]
    if email not in unique_contacts or \
       contact["created_at"] < unique_contacts[email]["created_at"]:
        unique_contacts[email] = contact

result = list(unique_contacts.values())

Backend Data Consistency Resolution:

Step 1: Merge Duplicate Records Use the bulk merge API to consolidate duplicates:

POST /api/admin/contacts/merge-duplicates
{
  "match_field": "email",
  "match_strategy": "exact",
  "merge_strategy": {
    "master_selection": "oldest",
    "field_resolution": "prefer_master",
    "preserve_relationships": true
  },
  "batch_size": 1000
}

Response:
{
  "job_id": "merge_job_xyz",
  "status": "processing",
  "estimated_duration": "45 minutes"
}

This automatically merges all duplicate contacts, preserving the oldest record as master and transferring all relationships (opportunities, activities, etc.) to the master record.

Step 2: Monitor Merge Progress

GET /api/admin/contacts/merge-duplicates/merge_job_xyz

Response:
{
  "status": "completed",
  "duplicates_found": 1247,
  "records_merged": 1247,
  "master_records_retained": 1247,
  "duplicate_records_archived": 1247
}

Step 3: Rebuild Search Index After merging duplicates, refresh the search index:

POST /api/admin/contacts/search-index/rebuild
{
  "full_rebuild": true,
  "priority": "high"
}

Response:
{
  "job_id": "reindex_abc",
  "status": "queued",
  "estimated_duration": "60 minutes",
  "records_to_index": 248753
}

The full rebuild ensures the search index perfectly matches the database after the merge operation.

Step 4: Verify Resolution After reindexing completes, verify the duplicates are gone:

GET /api/contacts/search?email=john.doe@example.com

Response:
{
  "contacts": [
    {
      "id": 12345,  // Only one record now
      "email": "john.doe@example.com",
      "is_master": true,
      "merged_from": [67890]
    }
  ],
  "total": 1
}

Preventing Future Duplicates:

  1. Enable Real-Time Deduplication:
POST /api/admin/settings/contacts
{
  "duplicate_prevention": {
    "enabled": true,
    "match_fields": ["email", "phone"],
    "match_threshold": "exact",
    "block_duplicate_creation": true
  }
}
  1. Implement Pre-Import Validation: Before importing, check for existing records:
def validate_import(contacts):
    existing_emails = GET("/api/contacts/emails/bulk-check",
                          json={"emails": [c["email"] for c in contacts]})

    new_contacts = [c for c in contacts
                   if c["email"] not in existing_emails]
    updates = [c for c in contacts
              if c["email"] in existing_emails]

    return new_contacts, updates
  1. Schedule Regular Duplicate Scans:
POST /api/admin/schedules/create
{
  "task": "duplicate_detection",
  "frequency": "weekly",
  "auto_merge": false,
  "notify_on_duplicates": true
}

Complete Resolution Timeline:

  1. Immediate (5 min): Enable API query filters to work around duplicates
  2. Short-term (1 hour): Run duplicate merge job to consolidate records
  3. Medium-term (2 hours): Rebuild search index after merge completes
  4. Long-term (ongoing): Enable real-time deduplication and scheduled monitoring

After completing these steps, your contact search API will return consistent, deduplicated results matching the UI behavior, and your sales team’s lead assignment automation will function correctly with reliable contact IDs.


This draft is based on general Adobe Experience Cloud knowledge. It has not been verified against your specific version and environment. Practitioners: verify the steps and share your experience below.

This sounds like a search index inconsistency. The UI likely queries the primary database directly, while the API uses a search index that may not have been updated after your bulk import. Check if there’s a reindex operation you can trigger. Large imports often require manual index refresh to ensure the search API returns consistent results with the database.

Also verify that your import didn’t create records with slightly different field values that the deduplication rules didn’t catch. Even small differences like trailing spaces, case variations, or different phone number formats can result in separate records being created. Adobe Experience Cloud’s default deduplication only triggers on exact email matches during import, but other fields might differ. Check the full record details for both IDs to see if there are any variations.

I compared the full records for both IDs and they’re 100% identical - same email, name, company, phone, everything. So it’s not a field variation issue. Where would I trigger a search index refresh? I don’t see that option in the admin console.

Confirmed this resolves our issue — after rebuilding the AEP search index and adding a deduplication merge rule in Adobe Real-Time CDP, the API returned clean, unique contact records.

The search index refresh isn’t in the admin UI - it’s an API operation. You need admin credentials to call POST /api/admin/contacts/reindex. This forces the search index to rebuild from the primary database. It can take 30-60 minutes for large contact databases, during which search results might be incomplete. Schedule it during off-hours if possible. The duplicate issue should resolve once the reindex completes.

Before you reindex, you should also run the built-in duplicate detection tool to merge any actual duplicates in the database. Go to Contacts → Data Quality → Find Duplicates. This will identify records with matching emails and let you merge them. If you reindex without merging, the duplicates will persist. The tool shows you which ID will be kept as the master record after merging.

One more thing - check your import job logs to see if any records failed validation and were retried. Sometimes failed imports get retried automatically, and if the duplicate detection wasn’t working during the import window, you could end up with multiple copies of the same record. The job logs will show if any records were inserted multiple times. You can access them via Settings → Import History → Job Details.