Payloads
This page documents the payloads of regular webhook events:
conversation.*message.*client.*referral.received(Meta ads)
WhatsApp Flows (whatsapp.flows.*) use the same envelope, with resourceType: "flow_response", but their data and expected response are documented in WhatsApp Flows , since those events require your endpoint to respond with data to keep the flow going.
Common envelope
All these events reach your endpoint via a JSON POST with this structure:
{
"id": "9f8c...",
"event": "conversation.created",
"workspaceId": "ws_456",
"timestamp": "2026-05-06T19:00:00.000Z",
"source": "system.conversation.created",
"resourceType": "conversation",
"resourceId": "conv_123",
"changes": null,
"data": {}
} | Field | Type | Description |
|---|---|---|
id | string | Unique event identifier |
event | string | Name of the event that triggered the webhook |
workspaceId | string | Workspace where the event happened |
timestamp | string | ISO-8601 date of the event |
source | string | Origin of the event — see the source field |
resourceType | conversation \| message \| client \| flow_response | Type of resource affected |
resourceId | string \| null | Identifier of the affected resource |
changes | object \| null | Detected changes. null for creation/deletion events |
data | object | Normalized snapshot of the resource |
If you only want to verify that the webhook is authentic, go to Delivery and signatures .
The source field
Identifies the origin of the event. For conversation, message, and customer events, its value follows the format:
<origin>.<resource>.<action>[.<qualifier>] - origin: one of
api,dashboard,inbound,agent,system,scheduler,campaign. - resource:
client,conversation,message,audience,scheduled_event,webhook,moderation,supervisor. - action: verb / state (
created,updated,deleted,processed,dispatched, etc.). - qualifier (optional):
bulk,auto,campaign.
For WhatsApp Flows, source is always whatsapp.flows.
Use source only for auditing / debugging. The logical identity of the event (what changed and which resources it affects) lives in event, resourceType, resourceId, and changes. The channel platform (whatsapp, instagram, …) is read from data.conversation.platform for conversation/message events, not from source.
Except for whatsapp.flows, source does not include provider names (messagebird, meta, whatsapp) or internal code paths. It's stable: if we refactor the emitter file, the source does not change.
Common examples by event:
event | Possible source values |
|---|---|
client.created | api.client.created, inbound.client.created, campaign.client.created |
client.updated | api.client.updated, api.client.blocked, api.client.unblocked, api.client.strikes_reset, api.conversation.status_changed, dashboard.client.updated, dashboard.client.blocked, dashboard.client.unblocked, dashboard.client.strikes_reset, inbound.client.profile_updated, inbound.client.refreshed, inbound.client.identifiers_updated, agent.supervisor.applied, system.moderation.applied |
client.tags.updated | api.client.tagged.bulk, api.audience.added, api.audience.removed, api.audience.cleared, dashboard.client.updated |
client.customFields.updated | api.client.custom_fields_updated, inbound.client.custom_fields_updated, agent.client.confirmation_saved, dashboard.client.updated |
conversation.created | system.conversation.created, dashboard.conversation.created, campaign.conversation.created |
conversation.status.updated | api.conversation.updated, api.conversation.finished, api.conversation.marked_spam, api.conversation.archived, api.conversation.reactivated, dashboard.conversation.stopped, campaign.conversation.updated |
conversation.expired | system.conversation.expired_by_inactivity, agent.conversation.expired, api.conversation.archived.auto, api.conversation.updated |
conversation.owners.updated | api.conversation.operator_assigned, api.conversation.operators_set, dashboard.conversation.assistance_accepted |
conversation.tags.updated | api.conversation.tagged, dashboard.conversation.assistance_requested |
message.created | inbound.message.received, agent.message.generated, agent.message.processed, agent.message.dispatched, agent.message.service_sent, agent.message.summary_generated, campaign.message.received, dashboard.message.system_inserted, scheduler.scheduled_event.created |
message.updated | inbound.message.delivery_updated, inbound.message.delivery_failed, inbound.message.delivery_updated.campaign, agent.message.processed, agent.message.status_updated, agent.message.dispatched, dashboard.message.resent, scheduler.scheduled_event.processing, scheduler.scheduled_event.sent, scheduler.scheduled_event.failed, api.scheduled_event.cancelled |
whatsapp.flows.* | whatsapp.flows |
Conversations
Events:
| Event | changes |
|---|---|
conversation.created | null |
conversation.status.updated | { "status": { "before", "after" } } |
conversation.operation.updated | { "operation": { "before", "after" } } |
conversation.owners.updated | { "owners": { "added": [], "removed": [] } } |
conversation.tags.updated | { "tags": { "added": [], "removed": [] } } |
conversation.expired | { "isFinished": { "before", "after": true } } |
For conversation.* events, data always has:
{
"client": {
"id": "521234567890",
"phoneNumber": "521234567890",
"creationDate": "2026-05-06T19:00:00.000Z",
"lastUpdate": "2026-05-06T19:00:00.000Z",
"name": "Juan Pérez",
"firstname": "Juan",
"email": null
},
"conversation": {
"id": "conv_123",
"conversationId": "521234567890",
"canSendDirectMessage": true,
"workspaceId": "ws_456",
"channelId": "521555000111",
"contactName": "Juan Pérez",
"phoneNumber": "521234567890",
"topic": "",
"platform": "whatsapp",
"owners": [],
"tags": [],
"creationDate": "2026-05-06T19:00:00.000Z",
"lastUpdate": "2026-05-06T19:00:00.000Z",
"status": "open",
"operation": "automatic",
"messageCount": 1,
"messages": []
}
} | Field | Type | Description |
|---|---|---|
id | string \| null | Unique, canonical document ID |
conversationId | string \| null | Chat thread ID; it does not identify the client |
canSendDirectMessage | boolean | Can only be false on WhatsApp, Instagram, and Messenger/Facebook; calculated from the latest user message. Always true for internal chats |
workspaceId | string \| null | Owning workspace |
channelId | string \| null | Channel where it happened |
contactName | string \| null | Contact name |
phoneNumber | string \| null | Contact phone number |
topic | string | Conversation topic |
platform | string \| null | Contact channel: whatsapp, instagram, messenger, sms, etc. |
owners | string[] | Assigned owners, identified by email (not UID) |
tags | string[] | Assigned tags |
creationDate | string \| null | ISO-8601 date |
lastUpdate | string \| null | ISO-8601 date |
status | string \| null | Current state: open, pending, finished, blocked, spam, expired |
operation | string \| null | Conversation mode: automatic (bot-handled) or manual (operator-handled) |
messageCount | number | Number of messages |
messages | unknown[] | Serialized messages. Only present in conversation.* events |
Messages
Events:
| Event | changes |
|---|---|
message.created | null |
message.updated | Per-field changes: content, contentBlocks, contentType, direction, lastUpdate, role, status, files, images, owner |
message.deleted | null — declared in the catalog, but not emitted yet. Subscribing produces no events today. |
For message.* events, data contains client, conversation, and message. The conversation does not include messages[]; the affected message is in data.message.
{
"client": { "...": "WebhookClientSummary" },
"conversation": { "...": "WebhookConversation sin messages[]" },
"message": {
"id": "msg_789",
"content": "Hola, necesito ayuda con mi pedido",
"contentBlocks": [
{ "type": "text", "text": "Hola, necesito ayuda con mi pedido" }
],
"contentType": "text",
"creationDate": "2026-05-06T19:00:00.000Z",
"direction": "incoming",
"files": [],
"images": [],
"owner": null,
"lastUpdate": "2026-05-06T19:00:00.000Z",
"role": "user",
"status": "delivered"
}
} | Field | Type | Description |
|---|---|---|
id | string \| null | Message identifier |
content | string | Plain-text version of the message |
contentBlocks | unknown[] | Structured blocks |
contentType | string | text by default |
creationDate | string \| null | ISO-8601 date |
direction | incoming \| outgoing \| string \| null | Always normalized to incoming or outgoing. Values like received or sent are translated before being emitted |
files | unknown[] | Attached files |
images | unknown[] | Attached images |
owner | { "id": string } \| null | Message author. id is the operator's email when applicable |
lastUpdate | string \| null | ISO-8601 date |
role | string \| null | user, assistant, tool, etc. |
status | string \| null | Message status. Common values: received, sent, delivered, read, failed |
The message does not have text, timestamp, senderId, senderName, conversationId, or type. Those fields do not exist in the envelope — use the schema above.
Example of message.updated:
{
"event": "message.updated",
"resourceType": "message",
"resourceId": "msg_789",
"changes": {
"status": {
"before": "sent",
"after": "read"
}
},
"data": {
"client": {},
"conversation": {},
"message": {}
}
} Customers
Events:
| Event | changes |
|---|---|
client.created | null |
client.updated | Per basic-field changes: phoneNumber, name, firstname, lastname, email, birthdate, gender, company, country, state, city, address, postalCode, status, moderation, creationDate, lastUpdate, deleted |
client.owners.updated | { "owners": { "added": [], "removed": [] } } |
client.tags.updated | { "tags": { "added": [], "removed": [] } } |
client.customFields.updated | { "customFields": { "<field>": { "before", "after" } } } or { "customFields": { "<field>": { "added": [], "removed": [] } } } |
For client.* events, data is the normalized customer directly:
{
"id": "521234567890",
"workspaceId": "ws_456",
"phoneNumber": "521234567890",
"name": "Juan Pérez",
"firstname": "Juan",
"lastname": "Pérez",
"email": "juan@empresa.com",
"birthdate": null,
"gender": null,
"company": "Acme Inc.",
"country": "MX",
"state": null,
"city": null,
"address": null,
"postalCode": null,
"status": "active",
"moderation": {
"totalStrikes": 0,
"lastAction": null,
"lastSource": null,
"lastSeverity": null,
"lastFlaggedAt": null,
"lastCategories": [],
"lastMatchedTerms": [],
"blockedAt": null,
"blockedBy": null,
"blockedReason": null
},
"tags": ["vip"],
"owners": ["agente1@empresa.com"],
"customFields": {
"placas": "ABC123"
},
"creationDate": "2026-05-06T19:00:00.000Z",
"lastUpdate": "2026-05-06T19:00:00.000Z",
"deleted": false
} In customFields, list-type fields are truncated to the last 5 items inside data. In client.customFields.updated, the changes object is computed from the full values, with no truncation.
Blocking and moderation
status and moderation always travel inside data, and moderation is returned in full even when the customer has no incidents. Its fields are documented in Get a Customer .
When you block or unblock a customer you get a client.updated carrying changes.status, plus one conversation.status.updated per open conversation that was synced. If you only care about the block itself, filter by the event's source rather than by how many events arrived.
| Action | source on the client.updated |
|---|---|
| Block via the API | api.client.blocked |
| Unblock via the API | api.client.unblocked |
| Strike reset via the API | api.client.strikes_reset |
| Block / unblock from the dashboard | dashboard.client.blocked, dashboard.client.unblocked |
| Strike reset from the dashboard | dashboard.client.strikes_reset |
| Automatic block by moderation | system.moderation.applied |
| Conversation marked as spam | api.conversation.status_changed |
Example of client.customFields.updated:
{
"event": "client.customFields.updated",
"resourceType": "client",
"resourceId": "521234567890",
"changes": {
"customFields": {
"placas": {
"before": "ABC123",
"after": "XYZ789"
},
"visitas": {
"added": [
{
"id": "visit_5",
"creationDate": "2026-05-06T19:00:00.000Z",
"lastUpdate": "2026-05-06T19:00:00.000Z",
"content": "Visita de seguimiento"
}
],
"removed": []
}
}
},
"data": {
"id": "521234567890",
"customFields": {
"placas": "XYZ789",
"visitas": []
}
}
} Meta Ads
Event:
| Event | changes | resourceType |
|---|---|---|
referral.received | null | client |
Emitted when an inbound message arrives attributed to a Meta ad (Click-to-WhatsApp, Click-to-Messenger, or Instagram ads). Unlike client.* events, its data is not the normalized customer: it contains client, conversation, and a referral object with the structured ad attribution.
{
"event": "referral.received",
"resourceType": "client",
"resourceId": "client_sample_abc123",
"changes": null,
"data": {
"client": {
"id": "521234567890",
"phoneNumber": "521234567890",
"creationDate": "2026-05-06T19:00:00.000Z",
"lastUpdate": "2026-05-06T19:00:00.000Z",
"name": "Juan Pérez",
"firstname": "Juan",
"email": null
},
"conversation": {
"id": "conv_123",
"conversationId": "521234567890",
"platform": "whatsapp",
"channelId": "wb-123456789012345"
},
"referral": {
"platform": "whatsapp",
"source": "ad",
"ad_id": "120211234567890123",
"ctwa_clid": "ARAkLkA8rmlFeiCktEJQ-QTwRiyYHAFDLMNDBH0CD3qpjd0HR4irJ6LEkR7JwLzMDopn7vghDWqTXUYWmTzID29SrqLOgUSDRAsNH0_sample",
"ref": null,
"headline": "Anuncio de prueba",
"body": "Texto principal del anuncio de prueba.",
"media_type": "image",
"image_url": "https://example.com/ad-image.jpg",
"video_url": null,
"thumbnail_url": null,
"welcome_message": "Hola, quiero más información",
"user_text": "Hola, quiero más información",
"channelId": "wb-123456789012345",
"messageId": "wamid.sample",
"conversationId": "521234567890",
"receivedAt": "2026-05-06T19:00:00.000Z"
}
}
} | Field | Type | Description |
|---|---|---|
platform | string | Channel platform: whatsapp, messenger, instagram |
source | string \| null | Source type reported by Meta, usually ad |
ad_id | string \| null | Meta ad identifier |
ctwa_clid | string \| null | Click-to-WhatsApp click ID, useful for the Conversions API |
ref | string \| null | Ad ref parameter (Click-to-Messenger / m.me) when present |
headline | string \| null | Ad headline |
body | string \| null | Ad primary text |
media_type | string \| null | Ad media type: image, video, etc. |
image_url | string \| null | Ad image URL |
video_url | string \| null | Ad video URL |
thumbnail_url | string \| null | Ad thumbnail URL |
welcome_message | string \| null | Welcome message prefilled in the ad |
user_text | string \| null | Text the customer sent when starting the conversation |
channelId | string \| null | Channel where the message arrived |
messageId | string \| null | Identifier of the inbound message that triggered the event |
conversationId | string \| null | Conversation identifier |
receivedAt | string \| null | ISO-8601 date when the attributed message was received |
referral.received is only emitted on channels connected directly with Meta (Click-to-WhatsApp, Click-to-Messenger, and Instagram ads). Ad fields that Meta does not send arrive as null.