Event type names are dot-namespaced (resource.action), the same convention used by Stripe, GitHub, and Meta. Every event's JSON body carries an event field with this exact string.
Support matrix
Not every channel can emit every event β mostly because the underlying platform (WhatsApp, Telegram, Instagram, Messenger) simply doesn't expose it. The event picker in the dashboard greys out anything unsupported for your selected channel, so you can't accidentally subscribe to something that will never fire.
| Event | WA Official | WA Unofficial | Telegram | Webchat | ||
|---|---|---|---|---|---|---|
message.received | β | β | β | β | β | β |
message.sent | β | β | β | β | β | β |
message.status.sent | β | β | β | β | β | β |
message.status.delivered | β | β | β | β | β | β |
message.status.read | β | β | β | β | β | β |
message.status.failed | β | β | β | β | β | β |
message.reaction.added | β | β | β | β | β | β |
message.reaction.removed | β | β | β | β | β | β |
conversation.assigned | β | β | β | β | β | β |
conversation.resolved | β | β | β | β | β | β |
template.status_updated | β | β | β | β | β | β |
call.status_updated | β | β | β | β | β | β |
Event reference
| Event | When it fires |
|---|---|
message.received | Fires when a contact sends a new inbound message on any channel. |
message.sent | Fires when Chatmo sends an outbound message β covers a human agent typing in the dashboard, an AI agent reply, a Workflow automation, and a message sent through the Send Message API. Check the sender.type field in the payload to tell them apart. |
message.status.sent / .delivered / .read / .failed | WhatsApp-only delivery lifecycle for an outbound message, matching the ACK levels WhatsApp itself reports. Telegram, Instagram, and Messenger don't expose a delivery-receipt API, so these never fire for those channels. |
message.reaction.added / .removed | Fires when a contact or an agent reacts to a message with an emoji, or removes a previously-added reaction. |
conversation.assigned | Fires when a conversation is assigned to an agent or a team β whether done manually from the dashboard or automatically by an auto-assignment rule. |
conversation.resolved | Fires when a conversation is marked resolved, regardless of who or what resolved it (human agent, AI handoff, or a Workflow). |
template.status_updated | WhatsApp Official only. Fires when Meta's review of one of your message templates changes status (approved, rejected, paused, etc). |
call.status_updated | WhatsApp Official only. Fires on WhatsApp Business Calling status changes (ringing, in progress, missed, completed, and so on). |
The exact JSON shape of each event, per channel, is below.
Payload envelope
Every webhook request shares the same envelope:
Code
event matches one of the event types exactly. data's shape depends on the event β it's identical across channels except for a few noted fields. raw_payload is optional and only appears when you've turned it on β see below.
Raw provider payload
Each webhook endpoint has an Include raw provider payload toggle (off by default, in the Add/Edit Webhook form). When it's on, every delivery for that endpoint gets a raw_payload field β a sibling of data, not nested inside it β carrying the payload Chatmo itself received from the channel for that event, close to as-is.
Use data for anything it already covers, which is almost everything: the text, the file of a media message (data.media.url, stored in Chatmo), location, shared contacts, templates and button replies all have their own normalized block on every channel. Reach for raw_payload only when you need a provider-specific field that data doesn't expose. See Raw payload per channel below.
:::warning It's the channel's shape, not Chatmo's
raw_payload mirrors WhatsApp's / Telegram's / Meta's own webhook item for that event, field names and all. It is not normalized, not versioned by Chatmo, and can change whenever the provider changes their API β Chatmo doesn't guarantee its shape the way it does for data. Only turn this on if you're prepared to handle that, and treat it as channel-specific integration code, not a stable contract.
:::
raw_payload is only ever present for events Chatmo received from a channel. Events Chatmo itself originates never have one, regardless of the toggle:
Never has raw_payload | Why |
|---|---|
message.sent | Chatmo sent this message, so there's no inbound provider payload to attach. The one exception is a message typed on the linked phone (sent_via: "device", WhatsApp only), which carries the echo Chatmo received. |
conversation.assigned | Assignment is a Chatmo-internal action. |
conversation.resolved | Resolution is a Chatmo-internal action. |
| Any event on a Webchat channel | Webchat is Chatmo's own protocol, not a third-party provider β there's no external webhook body to mirror. |
The remaining events (message.received, message.reaction.added/.removed, message.status.*, template.status_updated, call.status_updated) do get one, on every channel that supports that event in the first place β see the support matrix.
:::tip Try it with Test
The Test button sends a realistic per-channel raw_payload too (when the toggle is on), built from the same shapes shown on this page β a good way to see the exact field names before you write any code. See Testing & Delivery Logs.
:::
message.received
Code
message.received and message.sent share this exact data shape on every channel. A partner can handle both with one parser.
Fields
| Field | Always present | Description |
|---|---|---|
message_id | β | Chatmo's message ID (UUID). Use it to de-duplicate deliveries. |
conversation_id | β | Chatmo's conversation ID (UUID). |
external_message_id | β | The channel's own message ID (wamid... on WhatsApp Official, TLGM... on Telegram, and so on). |
type | β | TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT, STICKER, LOCATION, CONTACT, TEMPLATE, CALL_REQUEST or CALL_ENDED. |
content | β | The text of the message, the caption of a media message, or a short readable summary. Empty string when there is none. See Content types. |
timestamp | β | When the message was sent, in UTC (RFC 3339). created_at on the envelope is when the webhook was delivered. |
sender | β | Who wrote the message. type is CONTACT for message.received, plus id/name of the contact. |
contact | β | The customer in this conversation: id, name, and phone when the contact has one (always on WhatsApp channels; Instagram, Facebook and Telegram contacts have none). |
media | β | The file of a media message. See media. |
location | β | A shared location. See location. |
contacts | β | Shared contact cards. See contacts. |
template | β | A template message. See template. |
button_reply | β | The button or list row the contact tapped. See button_reply. |
reply_to | β | The message this one quotes. See reply_to. |
referral | β | Click-to-WhatsApp / Instagram ad that started the chat. See referral. |
call | β | Call summary for CALL_REQUEST / CALL_ENDED. See call. |
Fields marked "β" are left out entirely (not sent as null) when they don't apply, so check for the key before reading it. New optional fields can be added over time; ignore keys you don't recognise.
Content types
type reflects what was actually sent. Read content for the text, then the block for that type:
type | content | Block to read |
|---|---|---|
TEXT | The message text. | button_reply when it came from a button tap. |
IMAGE / VIDEO / AUDIO / DOCUMENT / STICKER | The caption, or an empty string. | media |
LOCATION | "π <place name>", or "π Shared Location" for a plain pin drop. | location |
CONTACT | "π€ <contact name>" ("π€ <name> and N more" for several contacts). | contacts |
TEMPLATE | The template text (a text header, then the body). | template |
CALL_REQUEST / CALL_ENDED | A short call summary, may be empty. | call |
LOCATION and CONTACT arrive from WhatsApp Official, WhatsApp Unofficial and Telegram. Facebook and Instagram (Messenger Platform) have no location or contact-share message, so on those two channels a contact can send TEXT, IMAGE, VIDEO, AUDIO, DOCUMENT or STICKER. An inbound TEMPLATE comes from WhatsApp Unofficial, when another WhatsApp Business account sends your number a template or interactive message.
Type-specific blocks
media
Present for IMAGE, VIDEO, AUDIO, DOCUMENT and STICKER. Chatmo downloads every inbound file from the channel and stores a copy in its own storage, so media.url is a direct https://files.chatmo.io/... link on every channel. You don't need a provider access token, a bot token or a Graph API call to fetch it, and it doesn't expire like a provider CDN link.
Code
| Field | Description |
|---|---|
url | Direct download link to the file in Chatmo storage. |
id | Chatmo's file ID. Pass it as media.file_id to the Send Message API to send the same file again without re-uploading. |
file_name | Original file name, when the channel provides one (always for DOCUMENT). |
mime_type | For example image/jpeg, video/mp4, audio/ogg, application/pdf. |
file_size | Size in bytes. |
thumbnail_url | Small preview image, when Chatmo generated one. |
caption | Same as content. Only present when the sender added a caption. |
Only url is guaranteed. The other fields are filled when known. In the rare case Chatmo could not copy the file from the channel (a provider error at that moment), id is missing and url is either the provider's original link (Facebook / Instagram, short-lived) or the whole media block is left out. The provider's own reference is still in raw_payload if you have it turned on.
location
Present for LOCATION.
Code
latitude and longitude are always set. name and address come from the channel for a named place, otherwise Chatmo looks them up from the coordinates (best effort). static_map_url is a map preview image.
contacts
Present for CONTACT. Always an array, even for a single contact.
Code
vcard is only included when the channel sent the raw vCard text (WhatsApp Unofficial).
template
Present for TEMPLATE messages (and a WhatsApp Unofficial interactive message such as a button or list message). The body text is in content; the rest of the template is here:
Code
| Field | Description |
|---|---|
type | Inbound only (WhatsApp Unofficial): template, buttons, list or interactive. |
name / language / category | Template identity. Present on message.sent for templates you send from Chatmo (WhatsApp Official). |
id | Chatmo's template ID, on message.sent. |
components | The template parts in WhatsApp's component format (HEADER, FOOTER, BUTTONS, ...). The BODY text is in content. |
header_media_url | Header image/video/document, stored in Chatmo storage. |
variables | The values that filled the template placeholders, on message.sent. |
button_reply
Present when the contact tapped a quick-reply button, an interactive button or a list row (WhatsApp Official and Unofficial). type is TEXT and content is the button label.
Code
type is quick_reply (template button), button_reply (interactive button) or list_reply (list row, which also carries description). payload is the ID you set on the button.
reply_to
Present when the contact replied to (quoted) an earlier message.
Code
message_id is set when the quoted message is known to Chatmo; external_message_id is the channel's ID for it.
referral
Present on the first message of a chat that started from a Click-to-WhatsApp ad or an Instagram ad / post.
Code
call
Present for CALL_REQUEST and CALL_ENDED (WhatsApp Official calling): the call snapshot, for example call_id, direction, status and duration_seconds. See call.status_updated for live call status.
Raw payload per channel
Everything above is in data on every channel, so most integrations never need raw_payload. Turn it on only when you need a provider-specific field that data doesn't carry. For media, the raw payload holds the provider's own reference to the file, which works differently per channel:
| Channel | Raw field | To get the file from the provider |
|---|---|---|
| WhatsApp Official | message.image.id (or .video.id / .document.id / ...), an opaque media ID. Meta is gradually rolling out a sibling message.image.url field WABA-by-WABA (started 2025-11-12) that's a direct, access-token-authenticated download link, but it may still be absent on any given message. | If url is present, fetch it directly (with your access token). If not, call Meta's Graph API GET /{media-id} with your own access token to resolve id to a short-lived download URL. Simpler: use data.media.url. |
| WhatsApp Unofficial | data.media_url | Already a direct, hosted URL (the same file as data.media.url). |
| Telegram | message.photo[].file_id (or .video.file_id / .document.file_id), an opaque file ID. | Call the Bot API's getFile with your bot token to resolve it to a download path. Simpler: use data.media.url. |
| Facebook / Instagram | message.attachments[].payload.url | A short-lived Meta CDN link. Use data.media.url for a link that doesn't expire. |
Below is one raw_payload example per channel: a TEXT message (the simplest case) and an IMAGE message (to show the media reference). The other content types (VIDEO, DOCUMENT, LOCATION, CONTACT) follow the same object shape with a different nested field; the reference table under each channel lists their field names.
WhatsApp Official
Wrapped in Meta's full webhook envelope β object β entry β changes β value β exactly like Meta's own webhook reference, scoped down to the one message that triggered this delivery (entry/changes each have exactly one element).
Raw payload β TEXT
Code
Raw payload β IMAGE
Code
url is part of Meta's gradual rollout (see the table above) β it may be missing on your webhooks even for an IMAGE message. Always fall back to id when it's absent.
| Type | Raw field |
|---|---|
| VIDEO | ...value.messages[0].video.id, .mime_type, .caption, .url (gradual rollout) |
| DOCUMENT | ...value.messages[0].document.id, .mime_type, .filename, .caption, .url (gradual rollout) |
| LOCATION | ...value.messages[0].location.latitude, .longitude, .name, .address |
| CONTACT | ...value.messages[0].contacts[].name.formatted_name, .phones[].phone / .wa_id |
:::tip Chatmo reconstructs this, it doesn't forward Meta's original bytes
Meta batches multiple messages/statuses into one webhook call (value.messages can have more than one entry). Chatmo already splits a batch into one delivery per message before this reaches your endpoint, so raw_payload rebuilds the same object/entry/changes/value structure Meta documents around just the one message β field-for-field accurate, but not a byte-identical copy of what Meta originally sent (a metadata field Meta included that Chatmo didn't store comes back empty rather than fabricated).
:::
WhatsApp Unofficial
Raw payload β TEXT
Code
Raw payload β IMAGE
Code
| Type | Raw field |
|---|---|
| VIDEO | data.media_url, .mimetype, .seconds, .caption |
| DOCUMENT | data.media_url, .mimetype, .filename, .page_count, .caption |
| LOCATION | data.latitude, .longitude, .location_name, .location_address |
| CONTACT | data.vcard (the raw vCard text β name/phone are inside it) |
Telegram
Raw payload β TEXT
Code
Raw payload β IMAGE
Code
| Type | Raw field |
|---|---|
| VIDEO | message.video.file_id, .duration, .caption |
| DOCUMENT | message.document.file_id, .file_name, .caption |
| LOCATION | message.location.latitude, .longitude (or message.venue for a named place) |
| CONTACT | message.contact.phone_number, .first_name, .last_name |
Facebook Messenger
Wrapped in Meta's documented Messenger Platform shape β object: "page" β entry[] β messaging[] β see Meta's Messenger webhook reference. entry/messaging each have exactly one element, same reasoning as WhatsApp Official above.
Raw payload β TEXT
Code
Raw payload β IMAGE
Code
| Type | Raw field |
|---|---|
| VIDEO / AUDIO / DOCUMENT | ...messaging[0].message.attachments[0].type (video / audio / file), .payload.url |
No LOCATION or CONTACT message type β Messenger Platform doesn't send those as inbound messages.
Same Messenger Platform contract as Facebook, field names are identical β but note the top-level body is a JSON array, not a bare object, per Meta's Instagram webhook reference:
Raw payload β TEXT
Code
Raw payload β IMAGE
Code
| Type | Raw field |
|---|---|
| VIDEO / AUDIO / DOCUMENT | [0].entry[0].messaging[0].message.attachments[0].type, .payload.url |
No LOCATION or CONTACT message type, same as Facebook.
message.sent
Code
Same data shape as message.received: the same fields and the same type-specific blocks (media, location, template, reply_to, ...). A template you send from Chatmo carries the full template block (name, language, category, components, variables). The differences:
| Field | Description |
|---|---|
sender.type | AGENT (a human agent, a Workflow, the Send Message API, or the WhatsApp Business app on the linked phone) or AI_AGENT. id/name are the agent or AI agent when known. |
status | Delivery status at the moment it was sent: sent, delivered (Webchat), or failed when the channel rejected it. Follow-up changes arrive as message.status.* events (WhatsApp only). |
contact | The customer the message was sent to. |
sent_via | Only for automated or external sends: workflow, api (Send Message API) or device (typed on the phone in the WhatsApp / WhatsApp Business app). Missing for dashboard and AI agent replies. |
sent_via_api | true for Send Message API sends. Kept for older integrations; prefer sent_via. |
message.sent only carries raw_payload for sent_via: "device" (the echo Chatmo received from WhatsApp). Every other send was made by Chatmo itself, so there is no provider payload. Internal notes (private messages) never trigger message.sent.
message.status.delivered / .read / .sent / .failed
Code
status mirrors the event suffix (sent, delivered, read, failed). WhatsApp Official and WhatsApp Unofficial only.
Raw payload β WhatsApp Official
Same object/entry/changes/value envelope as message.received, with value.statuses instead of value.messages (status webhooks arrive under the same "field": "messages" change per Meta's model):
Code
Raw payload β WhatsApp Unofficial
Code
message.reaction.added / .removed
Code
emoji is an empty string on message.reaction.removed. reactor.type is CONTACT, AGENT, or AI_AGENT. raw_payload is only present when the reactor is a contact β an agent's own reaction has no inbound provider payload to attach, same reasoning as message.sent.
Raw payload β WhatsApp Official
Meta delivers a reaction as a messages[] entry with type: "reaction" β same envelope as message.received:
Code
An empty "emoji": "" means the reaction was removed.
Raw payload β WhatsApp Unofficial
Code
Raw payload β Telegram
Code
An empty new_reaction array means the reaction was removed.
Raw payload β Facebook Messenger
Meta delivers a reaction as a messaging[] entry with reaction instead of message β same envelope as message.received:
Code
action is "unreact" on removal.
Raw payload β Instagram
Same shape as Facebook, wrapped in the array top-level Instagram uses everywhere else on this page:
Code
conversation.assigned
Code
assigned_to.type is USER or TEAM. This is a Chatmo-internal action β never carries a raw_payload.
conversation.resolved
Code
resolved_by is omitted when the conversation was resolved automatically (AI or Workflow) rather than by a human agent. This is also a Chatmo-internal action β never carries a raw_payload.
template.status_updated
Code
WhatsApp Official only. status mirrors Meta's template review states (APPROVED, REJECTED, PAUSED, DISABLED, PENDING_DELETION, IN_APPEAL, PENDING).
Raw payload
Code
call.status_updated
Code
WhatsApp Official only. status is one of RINGING, IN_PROGRESS, COMPLETED, MISSED, NO_ANSWER, REJECTED, CANCELED, FAILED.
Raw payload
Code
