Bulk insert to marketing campaigns API fails with 'duplicate email' error for valid new contacts

We’re using the bulk insert API to add contacts to marketing campaigns, but encountering a frustrating duplicate email error that doesn’t make sense. When we submit a batch of 100 new contacts to add to a campaign, about 30-40% of them fail with {"status": "error", "code": "DUPLICATE_DATA", "message": "duplicate email found"} even though these are brand new contacts that don’t exist in our CRM yet.

Here’s our bulk API call structure:

POST /crm/v2/Contacts/upsert
{
  "data": [
    {"Email": "new.contact@example.com", "Last_Name": "Contact", "Campaign_ID": "4876000000654321"},
    // ... 99 more contacts
  ],
  "duplicate_check_fields": ["Email"]
}

The error response doesn’t indicate which existing record has the duplicate email, making it impossible to troubleshoot. We’ve verified these emails don’t exist in Contacts, Leads, or any other module by searching the UI. Is the duplicate check happening against the CRM database or just within the current batch? Also, we need these contacts to be added to the campaign even if they somehow exist elsewhere - is there an ‘update existing’ option in the bulk API that would handle this gracefully instead of failing?

Adding contacts to campaigns through the Contacts upsert endpoint isn’t the right approach. Campaigns in Zoho have a separate relationship structure. You should be using the Campaign Members API instead: POST /crm/v2/Campaigns/{campaign_id}/Members. This endpoint is specifically designed for adding contacts to campaigns and handles duplicates properly - it won’t fail if a contact is already a campaign member, it just skips them or updates their member status.

We are using the upsert endpoint already (see the API call above), but it’s still throwing duplicate errors instead of updating existing records. Could there be a permission issue? Or maybe the Campaign_ID field can’t be updated via upsert? I need to understand why upsert is behaving like insert and rejecting duplicates rather than updating them.

The duplicate check in bulk API operations checks against all records in the module, not just your current batch. But if you’re sure the emails don’t exist, check if you have any duplicate detection rules configured in Setup > Customization > Modules > Contacts > Duplicate Check. Sometimes custom duplicate rules check across multiple fields or use fuzzy matching that catches more than you expect.

For the ‘update existing’ functionality, use the upsert endpoint instead of insert. The upsert operation will create new records if they don’t exist or update existing ones if they do, based on the duplicate_check_fields you specify. This is exactly what you need for campaign contact additions where you want to handle both new and existing contacts gracefully.

The duplicate email errors you’re experiencing stem from how the bulk API handles duplicate detection and the incorrect endpoint usage for campaign member management.

Duplicate Check Against CRM: The bulk upsert API checks for duplicates across the entire Contacts module in your CRM, not just within the current batch. The duplicate_check_fields parameter determines which fields are used for matching. When you specify ["Email"], the API searches for any existing Contact record with a matching email address.

However, the duplicate check also considers:

  • Soft-deleted records (in the Recycle Bin) that still exist in the database
  • Records in other modules if your duplicate detection rules span multiple modules
  • Case-insensitive email matching (“email@example.com” matches “EMAIL@EXAMPLE.COM”)

To diagnose which records are causing duplicates, use the search API before your bulk insert: GET /crm/v2/Contacts/search?email=new.contact@example.com to verify if records exist that aren’t visible in your UI search.

Bulk API Error Handling: The bulk API’s error responses are limited in detail for performance reasons. When processing 100 records, individual failures don’t include full diagnostic information. To get better error details:

  1. Reduce batch size to 10-20 records when troubleshooting
  2. Enable detailed error responses by adding trigger parameter: `“trigger”: [“approval”, “workflow”, “blueprint”]
  3. Check the response’s details array which contains per-record status

The upsert endpoint should update existing records, but it will fail if:

  • The existing record has field-level security preventing updates
  • Required fields are missing in your payload that weren’t required during initial creation
  • Workflow validation rules fail on the existing record

Update Existing Option: For proper campaign member management, you’re using the wrong endpoint. The Contacts upsert endpoint with Campaign_ID field doesn’t establish campaign membership correctly. Instead, use the Campaign Members API:

POST /crm/v2/Campaigns/{campaign_id}/Actions/add_members
{
  "Members": [
    {"Email": "contact@example.com"},
    {"Email": "another@example.com"}
  ]
}

This endpoint:

  • Automatically handles duplicates (won’t fail if contact already is a member)
  • Links existing contacts by email without requiring Contact ID
  • Creates new contacts if they don’t exist (depending on settings)
  • Properly establishes campaign-contact relationships

If you need to bulk import contacts AND add them to campaigns simultaneously, use a two-step process:

  1. First, bulk upsert contacts with trigger workflows disabled to avoid duplicate errors from validation rules
  2. Then, use the campaign members API to associate them with campaigns

For the upsert operation specifically, modify your approach to handle duplicates gracefully:

POST /crm/v2/Contacts/upsert
{
  "data": [...],
  "duplicate_check_fields": ["Email"],
  "skip_mandatory": false,
  "trigger": []
}

Set trigger to empty array to bypass workflow rules during bulk import, which often cause unexpected duplicate detection. After importing, run a separate API call to add the contacts to campaigns using their newly created IDs or email addresses via the Campaign Members endpoint.

This separation of concerns - contact creation vs. campaign membership - aligns with Zoho’s API design and prevents the duplicate errors you’re experiencing.

The upsert endpoint’s behavior depends on the duplicate_check_fields parameter. If it finds a duplicate based on those fields but can’t update certain fields in the existing record (due to field-level permissions or read-only fields), it will throw an error instead of partially updating. Check if the Campaign_ID field or any other fields in your payload have update restrictions. Also, verify your API user has edit permissions on all fields you’re trying to update.