Message Events
message.inbound.* and message.outbound.* - a message arrived, changed, or was deleted
Six events, split by direction. Direction is decided by the message itself: anything from the contact is inbound, anything sent by an operator, a flow, an AI assistant, or your own API call is outbound.
Subscribe with the enum names MESSAGE_INBOUND_CREATED, MESSAGE_INBOUND_UPDATED, MESSAGE_INBOUND_DELETED, MESSAGE_OUTBOUND_CREATED, MESSAGE_OUTBOUND_UPDATED, MESSAGE_OUTBOUND_DELETED.
message.inbound.created
type: message.inbound.created
A contact sent a message on any connected channel.
data
type Data = {
chat: ChatPayload;
message: MessagePayload; // direction is always "INBOUND"
contact?: ContactPayload; // present when the sender resolves to a contact
};See ChatPayload, MessagePayload, ContactPayload.
Example
{
"id": "evt_1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"type": "message.inbound.created",
"timestamp": "2026-09-30T18:00:00.000Z",
"data": {
"chat": {
"id": "6804f4c2a6f9f35f6e66f1a1",
"integrationId": "67f95b0126a4d1c9e3f0aa12",
"channel": "WHATSAPP",
"whatsappId": "wa_99887766",
"firstName": "Alex",
"conversationId": "6804f4d5a6f9f35f6e66f1c4",
"contactId": "6804f4d5a6f9f35f6e66f1a2"
},
"message": {
"id": "6804f4f01c34d0012ab8a055",
"chatId": "6804f4c2a6f9f35f6e66f1a1",
"direction": "INBOUND",
"type": "TEXT",
"text": "where is my order?",
"receivedAt": "2026-09-30T18:00:00.000Z",
"conversationId": "6804f4d5a6f9f35f6e66f1c4",
"contactId": "6804f4d5a6f9f35f6e66f1a2",
"peopleId": "6804f4d5a6f9f35f6e66f1a2",
"channel": "WHATSAPP",
"fromModel": "People",
"author": { "kind": "People", "id": "6804f4d5a6f9f35f6e66f1a2" },
"raw": { "providerMessageId": "wamid.HBgLMTU1NTEyMzQ1NjcVAgAS" }
},
"contact": {
"peopleId": "6804f4d5a6f9f35f6e66f1a2",
"phoneNumber": "+15551234567",
"firstName": "Alex",
"contactId": "6804f4d5a6f9f35f6e66f1a2"
}
},
"meta": {
"eventId": "evt_1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809",
"event": "message.inbound.created",
"occurredAt": "2026-09-30T18:00:00.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}This is the event you reply to. Take data.chat.id and pass it as chatId to sendMessage.
message.outbound.created
type: message.outbound.created
A message was sent to the contact - by an operator in the dashboard, by a flow, by an AI assistant, or by your own sendMessage call.
data - identical to message.inbound.created, with message.direction always "OUTBOUND" and sentAt set instead of receivedAt.
Check author.kind to see who replied. A human operator is User, the AI assistant is AIAssistant (Bot is a flow or bot, not the AI). Both arrive as message.outbound.created, and nothing else in the payload separates them - so this is how you label a reply in your own UI, or decide whether to count it against an operator's workload.
Your own sends come back to you as message.outbound.created. If you mirror blindly you will duplicate messages you already have. Deduplicate on the _id returned by the mutation, which equals data.message.id here.
message.inbound.updated / message.outbound.updated
type: message.inbound.updated · message.outbound.updated
The message content changed - an edit.
message.outbound.updated no longer carries delivery-status changes. Those moved to the opt-in message.outbound.status event. If you were reading deliveryStatus here, subscribe to that event or you will stop seeing status transitions.
data
type Data = {
chat: ChatPayload;
message: MessagePayload;
contact?: ContactPayload;
previous?: Partial<MessagePayload>; // only the fields that changed
};previous holds the prior values of the changed fields, when the emitting code supplied them. Diff against it rather than re-reading the whole message.
Example - an edit with the prior values
{
"id": "evt_5f6a7b8c-9d0e-1f20-3a4b-5c6d7e8f9012",
"type": "message.outbound.updated",
"timestamp": "2026-09-30T18:00:04.000Z",
"data": {
"chat": { "id": "6804f4c2a6f9f35f6e66f1a1", "channel": "WHATSAPP" },
"message": {
"id": "6804f5011c34d0012ab8a077",
"chatId": "6804f4c2a6f9f35f6e66f1a1",
"direction": "OUTBOUND",
"type": "TEXT",
"text": "Your pizza is on the way - about 15 minutes 🍕",
"sentAt": "2026-09-30T18:00:00.000Z"
},
"previous": { "text": "Your pizza is on the way 🍕" }
},
"meta": {
"eventId": "evt_5f6a7b8c-9d0e-1f20-3a4b-5c6d7e8f9012",
"event": "message.outbound.updated",
"occurredAt": "2026-09-30T18:00:04.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}A failed send surfaces on message.outbound.status with errorMessage set. It is not a webhook error and not a 4xx on your endpoint - see Channel constraints → Delivery failures.
message.inbound.deleted / message.outbound.deleted
type: message.inbound.deleted · message.outbound.deleted
The message was hard-deleted. The payload is deliberately minimal - the full message no longer exists to describe.
data
type Data = {
chat: {
id: string;
integrationId?: string;
channel?: "WHATSAPP" | "TELEGRAM" | "VIBER" | "INSTAGRAM" | "WEB";
};
message: {
id: string;
chatId: string;
direction: "INBOUND" | "OUTBOUND";
type: MessageType;
};
deletedAt: string; // ISO-8601 UTC
};Example
{
"id": "evt_9a0b1c2d-3e4f-5061-7283-94a5b6c7d8e9",
"type": "message.inbound.deleted",
"timestamp": "2026-09-30T18:02:11.000Z",
"data": {
"chat": {
"id": "6804f4c2a6f9f35f6e66f1a1",
"integrationId": "67f95b0126a4d1c9e3f0aa12",
"channel": "WHATSAPP"
},
"message": {
"id": "6804f4f01c34d0012ab8a055",
"chatId": "6804f4c2a6f9f35f6e66f1a1",
"direction": "INBOUND",
"type": "TEXT"
},
"deletedAt": "2026-09-30T18:02:11.000Z"
},
"meta": {
"eventId": "evt_9a0b1c2d-3e4f-5061-7283-94a5b6c7d8e9",
"event": "message.inbound.deleted",
"occurredAt": "2026-09-30T18:02:11.000Z",
"organisationId": "6716b2f0a1c34d0012ab89ef",
"externalId": "crmco-client-5541",
"partnerId": "6716b1001c34d0012ab89abc",
"apiVersion": "2026-04-01",
"attempt": 1
}
}There is no contact and no text on a delete. Match on data.message.id against what you already stored.