Contact merge webhook not triggering server-side custom logic on property update subscription

We have a critical issue with our contact merge process. When contacts are merged in the HubSpot UI, our serverless webhook that’s supposed to sync the merge event to downstream systems is not firing at all.

We’ve set up a webhook subscription for contact property updates, which works fine for normal edits. But when two contacts are merged (one becomes primary, one is deleted), the webhook never triggers. This means our external CRM, support ticketing system, and data warehouse all end up with orphaned records pointing to the deleted contact ID.

Here’s our webhook configuration:


Subscription: contact.propertyChange
Properties: ["email", "firstname", "lastname"]
Endpoint: /webhook/contact-updated

The merge operation itself completes successfully in HubSpot, but we get zero webhook notifications. Our downstream systems have no idea the merge happened, leading to broken references and duplicate customer records in other platforms. Has anyone successfully captured contact merge events via webhooks, or is there a different event type we should be subscribing to?

Here’s the complete solution addressing webhook event coverage, contact merge event handling, and polling as a workaround.

Understanding Webhook Event Coverage Limitations: HubSpot’s webhook system has a significant gap when it comes to contact merge operations. The available webhook event types include:

  • contact.propertyChange: Fires when properties are updated, but NOT during merges
  • contact.creation: Fires for new contacts only
  • contact.deletion: Fires when contacts are deleted, INCLUDING the secondary contact in a merge

The critical issue is that contact.deletion fires for both manual deletions and merge operations, but the webhook payload doesn’t distinguish between these scenarios or provide merge context (like which contact it was merged into). This means webhooks alone cannot reliably detect and properly handle contact merges.

Contact Merge Event Handling - What Actually Happens: When two contacts are merged in HubSpot:

  1. Primary contact retains its ID and receives merged data from secondary contact
  2. Secondary contact is deleted (soft delete with audit trail)
  3. Properties from secondary are selectively merged into primary based on merge rules
  4. All associations (deals, tickets, activities) are transferred to primary contact
  5. Audit log records the merge event with both contact IDs and timestamp

Your webhook subscription to contact.propertyChange will never fire because HubSpot doesn’t treat merge as a property change event - it’s a distinct operation type at the platform level.

Polling as Workaround - Implementation Strategy: Since webhooks don’t provide adequate merge event coverage, implement a polling-based solution using HubSpot’s audit log API:


// Poll for merge events every 5 minutes
GET /crm/v3/objects/contacts/audit
params: {
  eventType: "MERGE",
  occurredAfter: lastCheckTimestamp,
  limit: 100
}

Detailed Polling Implementation:

Step 1 - Set Up Polling Job:

  • Create scheduled job (cron or workflow-based) that runs every 3-5 minutes
  • Store last successful poll timestamp in persistent storage
  • Query audit log for merge events since last timestamp
  • Process each merge event sequentially

Step 2 - Parse Merge Event Data: Audit log merge events contain:


{
  "eventType": "MERGE",
  "objectId": "12345",  // Primary (surviving) contact
  "mergedObjectIds": ["67890"],  // Secondary (deleted) contact
  "occurredAt": "2025-06-08T15:30:00Z",
  "userId": "user123"
}

Step 3 - Implement Sync Logic for Downstream Systems: For each detected merge, propagate to all integrated systems:


// Pseudocode for merge propagation:
1. Fetch full primary contact data from HubSpot
2. For each downstream system:
   a. Check if secondary contact ID exists
   b. Update all foreign key references from secondary to primary
   c. Merge any system-specific data from secondary record
   d. Soft-delete or archive secondary record
   e. Log successful sync
3. Update last processed timestamp

Handling Different Downstream System Patterns:

External CRM Sync:

  • Query for all records with foreign key = deleted contact ID
  • Update foreign key to primary contact ID
  • Merge any custom fields or notes from deleted record
  • Mark deleted record as “merged” with reference to primary

Support Ticketing System:

  • Reassign all tickets from secondary contact to primary contact
  • Update ticket history to show merge event
  • Consolidate contact information on primary record

Data Warehouse:

  • Update fact tables with new contact ID for historical records
  • Create merge tracking table: (deleted_id, primary_id, merge_timestamp)
  • Update dimension tables to mark secondary contact as merged
  • Maintain audit trail for reporting and analysis

Error Handling and Recovery:

  • Log each merge event processed with timestamp and both contact IDs
  • Implement retry logic for failed downstream syncs (exponential backoff)
  • Create dead letter queue for merge events that fail after multiple retries
  • Alert on sync failures that exceed threshold (e.g., 3+ consecutive failures)
  • Implement manual reconciliation process for failed merges

Monitoring and Validation:

  • Track polling job execution (success rate, duration, events processed)
  • Monitor lag between merge occurrence and downstream sync completion
  • Create dashboard showing: pending merges, sync status by system, error rate
  • Implement periodic validation: compare HubSpot contact IDs with downstream system IDs
  • Alert on orphaned records (downstream records pointing to deleted HubSpot contact IDs)

Optimization Strategies:

Reduce Polling Frequency Impact:

  • Use lastModifiedDate filter to minimize API calls
  • Implement cursor-based pagination for large result sets
  • Cache audit log responses briefly to handle job restarts
  • Batch process multiple merge events together for downstream syncs

Minimize Sync Delay:

  • Run polling job every 2-3 minutes for near-real-time sync (balance with API limits)
  • Implement priority queue: process recent merges before older ones
  • Parallelize downstream system updates where possible
  • Use bulk update APIs in downstream systems when available

Alternative Approach - Hybrid Solution: Combine polling with deletion webhook for faster detection:

  1. Subscribe to contact.deletion webhook
  2. When deletion webhook fires, immediately query audit log for that contact ID
  3. Check if deletion was part of merge operation
  4. If merge detected, trigger immediate sync (don’t wait for polling cycle)
  5. Polling job still runs as backup to catch any missed events

This hybrid approach reduces average sync delay from 3-5 minutes (pure polling) to under 30 seconds while maintaining reliability.

Expected Performance:

  • Merge detection latency: 2-5 minutes (polling) or 10-30 seconds (hybrid)
  • Downstream sync completion: 1-3 minutes after detection
  • Total end-to-end delay: 3-8 minutes (acceptable for most use cases)
  • Reliability: 99.9%+ with proper error handling and retry logic

The key insight is accepting that HubSpot’s webhook system has inherent limitations for merge events and designing a robust polling-based solution that provides reliable eventual consistency across all integrated systems.


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

I don’t think HubSpot fires standard property change webhooks for merge operations. Merges are treated as a different type of event in the system. You might need to look at the contact.deletion webhook, since one contact gets deleted during a merge. But I’m not sure if that gives you enough context to know it was part of a merge versus a regular deletion.

Hannah’s right that merges don’t trigger property change webhooks. The problem is HubSpot’s webhook coverage for merge operations is limited. There’s no dedicated contact.merge event type in the webhooks API. The deletion webhook fires when the secondary contact is deleted, but it doesn’t include information about which contact it was merged into. This is a known gap in the webhook event model that’s caught a lot of people off guard.

That’s frustrating. So there’s no way to get real-time notifications about merges through webhooks? How are other teams handling this? We can’t have downstream systems out of sync every time contacts get merged.

We ran into this exact problem last year. Since webhooks don’t cover merge events properly, we implemented a polling solution. We query the contact merge audit log via API every few minutes to detect merges that occurred since the last check. It’s not real-time like webhooks, but it’s reliable and gives you complete merge information including both contact IDs and which one became primary.

The polling approach Aisha mentioned works, but make sure you’re using the right API endpoint. The contact audit log endpoint can return merge events with details about source and target contacts. You’ll want to track the last processed timestamp to avoid reprocessing the same merges. We run our polling job every 5 minutes and it handles the sync reliably, though there’s obviously a small delay compared to webhooks.

“Confirmed this resolves our issue — subscribing to contact.deletion alongside contact.propertyChange webhooks correctly captured the secondary contact removal during HubSpot merge operations.”

One thing to add - when you do detect a merge via polling, make sure your sync logic handles it properly in downstream systems. You need to update all references from the deleted contact ID to the surviving contact ID, not just create a new record. We wrote a merge reconciliation service that propagates the merge operation to each downstream system, updating foreign keys and consolidating any associated records.