Skip to main content

Contact Deduplicated Event

Triggered when WhatsApp reveals that two contacts (one identified by LID, one by phone number) are actually the same person. When this happens, Zapy automatically merges the duplicate contacts and notifies you so you can update your local database.

Event Type

contact-deduplicated

Why This Happens

WhatsApp uses two types of identifiers:

  • LID (Local ID): A privacy-preserving identifier that doesn't reveal the phone number
  • JID (Jabber ID): The traditional phone number-based identifier (e.g., 5511999999999@s.whatsapp.net)

When a contact first messages you, WhatsApp may only provide the LID. Later, WhatsApp may reveal the actual phone number (JID) through a LID-to-PN (phone number) mapping. If you already had a contact with that phone number, you now have duplicate records for the same person.

Zapy automatically:

  1. Detects the LID-to-PN mapping
  2. Merges the contacts (keeping the one with the LID as the primary)
  3. Sends you this webhook so you can update your database

Payload Structure

{
"event": "contact-deduplicated",
"instanceId": "instance-uuid",
"data": {
"instanceId": "instance-uuid",
"keptContactId": "kept-contact-uuid",
"deletedContactId": "deleted-contact-uuid",
"lid": "2462467481759420393427@lid",
"pn": "5511999999999@s.whatsapp.net",
"timestamp": "2024-12-14T12:00:00.000Z"
}
}

Data Fields

FieldTypeDescription
instanceIdstringInstance ID where the deduplication occurred
keptContactIdstringThe contact UUID that was kept (merged into) - use this going forward
deletedContactIdstringThe contact UUID that was deleted (merged from) - update references to this
lidstringThe LID that triggered the deduplication
pnstringThe phone number JID associated with the LID
timestampstringISO 8601 timestamp of the event

Handling Deduplication

When you receive this event, you need to update your local database to reflect the merge:

1. Update References

Any references to deletedContactId should be updated to point to keptContactId:

app.post('/webhook', async (req, res) => {
const { event, data } = req.body;

if (event === 'contact-deduplicated') {
const { keptContactId, deletedContactId, lid, pn } = data;

// Update all references to the deleted contact
await db.conversations.updateMany(
{ contactId: deletedContactId },
{ contactId: keptContactId }
);

await db.messages.updateMany(
{ contactId: deletedContactId },
{ contactId: keptContactId }
);

// Delete the duplicate contact from your database
await db.contacts.delete({ externalId: deletedContactId });

// Optionally update the kept contact with the phone number
await db.contacts.update(
{ externalId: keptContactId },
{ phoneNumber: pn.replace('@s.whatsapp.net', '') }
);

console.log(`Merged contact ${deletedContactId} into ${keptContactId}`);
}

res.status(200).send('OK');
});

2. Type-Safe Handling with SDK

import {
ZapyWebhookPayload,
ZapyEventTypes
} from '@zapyapi/sdk';

function handleWebhook(payload: ZapyWebhookPayload) {
if (payload.event === ZapyEventTypes.CONTACT_DEDUPLICATED) {
const { keptContactId, deletedContactId, lid, pn } = payload.data;

console.log(`Contact deduplication detected:`);
console.log(` - Kept: ${keptContactId}`);
console.log(` - Deleted: ${deletedContactId}`);
console.log(` - LID: ${lid}`);
console.log(` - Phone: ${pn}`);

// Update your local database...
}
}

Best Practices

  1. Always handle this event - If you sync contacts locally, you must handle this event to avoid orphaned records and broken references.

  2. Update all references atomically - Use database transactions to ensure all references are updated together.

  3. Use keptContactId going forward - After receiving this event, always use keptContactId for future operations.

  4. Log for debugging - Keep a log of deduplication events to help debug any inconsistencies.

  5. Handle idempotently - Your handler should be safe to run multiple times with the same payload (in case of webhook retries).

Example: Complete Handler

import {
ZapyWebhookPayload,
ZapyEventTypes,
ZapyContactDeduplicatedEvent
} from '@zapyapi/sdk';

async function handleContactDeduplicated(data: ZapyContactDeduplicatedEvent) {
const { keptContactId, deletedContactId, pn } = data;

// Use a transaction for data integrity
await db.transaction(async (tx) => {
// Check if we have the deleted contact
const deletedContact = await tx.contacts.findOne({
externalId: deletedContactId
});

if (!deletedContact) {
// Already processed or contact doesn't exist locally
console.log(`Contact ${deletedContactId} not found, skipping`);
return;
}

// Update all foreign key references
await tx.conversations.updateMany(
{ contactId: deletedContactId },
{ contactId: keptContactId }
);

await tx.messages.updateMany(
{ senderId: deletedContactId },
{ senderId: keptContactId }
);

// Merge any unique data from deleted contact to kept contact
const keptContact = await tx.contacts.findOne({
externalId: keptContactId
});

if (keptContact) {
// Update with phone number from the mapping
await tx.contacts.update(
{ externalId: keptContactId },
{
phoneNumber: pn.replace('@s.whatsapp.net', ''),
// Preserve any custom fields from deleted contact if not set
notes: keptContact.notes || deletedContact.notes,
tags: [...new Set([...keptContact.tags, ...deletedContact.tags])]
}
);
}

// Finally, delete the duplicate
await tx.contacts.delete({ externalId: deletedContactId });

console.log(`Successfully merged ${deletedContactId} into ${keptContactId}`);
});
}