Contact Events
contact.created, updated, deleted, merged, split - the lifecycle and identity of a person
Five events. The first three are the ordinary lifecycle; contact.merged and contact.split exist because Wexio can decide that two contacts are the same person, or undo that decision.
Subscribe with CONTACT_CREATED, CONTACT_UPDATED, CONTACT_DELETED, CONTACT_MERGED, CONTACT_SPLIT.
If you mirror contacts into your own product, subscribe to CONTACT_MERGED and CONTACT_SPLIT. Without them, two records you hold as separate people can become one person on Wexio's side and your copy silently diverges, with no contact.updated to tell you.
contact.created
type: contact.created
A new person record was created in the client org.
data
type Data = { contact: ContactPayload };See ContactPayload.
Example
{
"id": "evt_6f708192-a3b4-c5d6-e7f8-091223344556",
"type": "contact.created",
"timestamp": "2026-09-30T17:59:57.000Z",
"data": {
"contact": {
"peopleId": "6804f4d5a6f9f35f6e66f1a2",
"phoneNumber": "+15551234567",
"firstName": "Alex",
"createdAt": "2026-09-30T17:59:57.000Z",
"contactId": "6804f4d5a6f9f35f6e66f1a2"
}
},
"meta": {
"eventId": "evt_6f708192-a3b4-c5d6-e7f8-091223344556",
"event": "contact.created",
"occurredAt": "2026-09-30T17:59:57.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}Key your own record on contactId, not peopleId - see merges below for why.
contact.updated
type: contact.updated
A field on the person changed - name, phone, email, or a custom field.
data
type Data = {
contact: ContactPayload;
previous?: Partial<ContactPayload>; // prior values of the changed fields
};Example
{
"id": "evt_708192a3-b4c5-d6e7-f809-122334455667",
"type": "contact.updated",
"timestamp": "2026-09-30T18:05:00.000Z",
"data": {
"contact": {
"peopleId": "6804f4d5a6f9f35f6e66f1a2",
"phoneNumber": "+15551234567",
"email": "alex@example.com",
"firstName": "Alex",
"lastName": "Moreno",
"updatedAt": "2026-09-30T18:05:00.000Z",
"contactId": "6804f4d5a6f9f35f6e66f1a2"
},
"previous": { "email": null, "lastName": null }
},
"meta": {
"eventId": "evt_708192a3-b4c5-d6e7-f809-122334455667",
"event": "contact.updated",
"occurredAt": "2026-09-30T18:05:00.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}contact.deleted
type: contact.deleted
The person was removed. The payload carries only the ID - the record is being dropped, so there is nothing else to describe.
data
type Data = {
contact: { peopleId: string };
deletedAt: string; // ISO-8601 UTC
};Example
{
"id": "evt_8192a3b4-c5d6-e7f8-0912-233445566778",
"type": "contact.deleted",
"timestamp": "2026-09-30T18:30:00.000Z",
"data": {
"contact": { "peopleId": "6804f4d5a6f9f35f6e66f1a2" },
"deletedAt": "2026-09-30T18:30:00.000Z"
},
"meta": {
"eventId": "evt_8192a3b4-c5d6-e7f8-0912-233445566778",
"event": "contact.deleted",
"occurredAt": "2026-09-30T18:30:00.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}Removing a contact also removes their threads, so expect chat.deleted for each one around the same time. Ordering between the two is not guaranteed.
contact.merged
type: contact.merged
Wexio folded one or more contacts into a single surviving contact - the same person reached the client on two channels, or two records turned out to be one.
This event exists to give you a forwarding pointer. A bare contact.deleted for the losers could never tell you where their history went.
data
type Data = {
contact: {
from: string[]; // the retired (loser) contact ids
into: string; // the surviving (winner) contact id
};
mergedAt: string; // ISO-8601 UTC
};Example
{
"id": "evt_92a3b4c5-d6e7-f809-1223-344556677889",
"type": "contact.merged",
"timestamp": "2026-09-30T18:12:00.000Z",
"data": {
"contact": {
"from": ["6804f4d5a6f9f35f6e66f1a2", "6804f4d5a6f9f35f6e66f1b7"],
"into": "6804f4d5a6f9f35f6e66f1c9"
},
"mergedAt": "2026-09-30T18:12:00.000Z"
},
"meta": {
"eventId": "evt_92a3b4c5-d6e7-f809-1223-344556677889",
"event": "contact.merged",
"occurredAt": "2026-09-30T18:12:00.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}How to handle it
- Look up your local records for every ID in
from. - Re-point them at the record for
into, or merge them into it. - Keep the
fromIDs as aliases - historical messages you already stored still reference them.
from always has at least one entry, and into is never one of them.
contact.split
type: contact.split
A previous merge was undone. The mirror image of contact.merged.
data
type Data = {
contact: {
restored: string[]; // contact ids that are separate people again
from: string; // the winner they had been folded into
};
splitAt: string; // ISO-8601 UTC
};Example
{
"id": "evt_a3b4c5d6-e7f8-0912-2334-455667788990",
"type": "contact.split",
"timestamp": "2026-09-30T18:20:00.000Z",
"data": {
"contact": {
"restored": ["6804f4d5a6f9f35f6e66f1a2"],
"from": "6804f4d5a6f9f35f6e66f1c9"
},
"splitAt": "2026-09-30T18:20:00.000Z"
},
"meta": {
"eventId": "evt_a3b4c5d6-e7f8-0912-2334-455667788990",
"event": "contact.split",
"occurredAt": "2026-09-30T18:20:00.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}Note the field names are not symmetric with contact.merged: here from is the winner the contacts are being pulled out of, and restored lists the contacts that are separate again.
Both events are emitted after the merge or split has committed, so a read issued when you receive one already reflects the new state.