Webhooks

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

  1. Look up your local records for every ID in from.
  2. Re-point them at the record for into, or merge them into it.
  3. Keep the from IDs 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.

On this page