Payloads
This page documents the payloads of regular webhook events:
conversation.*message.*comment.*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 \| comment \| 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, comment, 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,comment,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. Read the channel platform from data.conversation.platform for conversation and message events, or from data.comment.platform for comments; do not derive it from source.
Except for whatsapp.flows, source does not depend on the provider or implementation details. It remains stable across implementation changes.
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 |
comment.created | inbound.comment.received |
comment.updated | inbound.comment.updated, dashboard.comment.updated |
comment.deleted | inbound.comment.deleted, dashboard.comment.deleted |
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 conversation 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 |
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 without 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": {}
}
} Comments
Events:
| Event | changes |
|---|---|
comment.created | null |
comment.updated | Per-field changes with { "before", "after" }: content, contentType, attachments, isSpam, moderation |
comment.deleted | null |
For comment.* events, resourceType is comment and data always contains client, post, thread, and comment. client is a slim summary when the author is associated with a customer, or null otherwise. The platform is available at data.comment.platform.
Example of comment.created:
{
"id": "evt_comment_demo_01",
"event": "comment.created",
"workspaceId": "ws_demo_456",
"timestamp": "2026-08-31T15:42:18.000Z",
"source": "inbound.comment.received",
"resourceType": "comment",
"resourceId": "comment_demo_01",
"changes": null,
"data": {
"client": {
"id": "client_demo_01",
"phoneNumber": null,
"creationDate": "2026-04-12T17:05:00.000Z",
"lastUpdate": "2026-08-31T15:42:18.000Z",
"name": "Ana López",
"firstname": "Ana",
"email": null
},
"post": {
"id": "post_demo_01",
"externalId": "18000000000123456",
"platform": "instagram",
"channelId": "channel_demo_01",
"title": "Which color do you prefer this summer?",
"permalink": "https://www.instagram.com/p/DemoPost123/",
"contactName": "Platica Demo"
},
"thread": {
"id": "comment_demo_01",
"status": "open",
"needsReply": true,
"commentCount": 1,
"topic": "Instagram comments",
"tags": []
},
"comment": {
"id": "comment_demo_01",
"postId": "18000000000123456",
"parentCommentId": null,
"rootCommentId": "comment_demo_01",
"permalink": "https://www.instagram.com/p/DemoPost123/c/DemoComment456/",
"platform": "instagram",
"channelId": "channel_demo_01",
"content": "I love the blue one 💙",
"contentType": "text",
"attachments": [],
"creationDate": "2026-08-31T15:42:18.000Z",
"lastUpdate": "2026-08-31T15:42:18.000Z",
"direction": "incoming",
"role": "user",
"clientId": "client_demo_01",
"authorName": "Ana López",
"authorPhotoUrl": "https://cdn.example.com/profiles/ana-demo.jpg",
"authorType": null,
"isSpam": false,
"moderation": {
"action": "none",
"synced": false,
"moderatedAt": null
}
}
}
} post summarizes the related post:
| Field | Type | Description |
|---|---|---|
id | string \| null | Post identifier |
externalId | string \| null | Content identifier on the platform |
platform | string \| null | instagram, facebook, or another supported platform |
channelId | string \| null | Connected channel |
title | string \| null | Post text or title |
permalink | string \| null | Public URL of the post |
contactName | string \| null | Display name for the channel account |
thread summarizes the comment thread:
| Field | Type | Description |
|---|---|---|
id | string \| null | Thread identifier; matches comment.rootCommentId |
status | string \| null | Current thread status |
needsReply | boolean | Whether the thread needs a reply |
commentCount | number | Number of comments in the thread |
topic | string \| null | Thread topic |
tags | string[] | Thread tags |
comment contains the affected comment:
| Field | Type | Description |
|---|---|---|
id | string \| null | Comment identifier |
postId | string \| null | External post identifier; matches post.externalId |
parentCommentId | string \| null | Parent comment when this is a reply |
rootCommentId | string \| null | Root comment in the thread |
permalink | string \| null | Public URL of the comment |
platform | string \| null | Comment platform |
channelId | string \| null | Connected channel |
content | string | Comment text |
contentType | string | Content type, usually text |
attachments | object[] | Public comment attachments |
creationDate | string \| null | ISO-8601 creation date |
lastUpdate | string \| null | ISO-8601 last update date |
direction | incoming \| outgoing \| null | incoming for customers and outgoing for the brand |
role | string \| null | Normalized author role |
clientId | string \| null | Associated customer, when available |
authorName | string \| null | Author display name |
authorPhotoUrl | string \| null | URL of the author's public photo |
authorType | string \| null | Author type |
isSpam | boolean | Whether the comment is marked as spam |
moderation | { "action": "none" \| "hidden" \| "deleted", "synced": boolean, "moderatedAt": string \| null } | Moderation state |
Each attachments item can include:
| Field | Type | Description |
|---|---|---|
id | string \| null | Attachment identifier |
type | string | image, video, link, or unsupported |
status | string | Attachment availability status |
url | string \| null | URL available to the integration |
mime | string \| null | MIME type, when available |
title | string \| null | Attachment title |
bytes | number \| null | Size in bytes, when available |
In comment.updated, each changed field includes its previous and new values. data keeps the same shape and contains the snapshot after the change. For example, the changes fragment for a moderation update can be:
{
"moderation": {
"before": {
"action": "none",
"synced": false,
"moderatedAt": null
},
"after": {
"action": "hidden",
"synced": true,
"moderatedAt": "2026-08-31T15:48:02.000Z"
}
}
} 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.