Event Types and Payload
Subscription names belong to the channel binding. The delivered eventType identifies the normalized event; it is not a webhook CRUD field.
Subscriptions and delivered events
| Provider | Subscribe to | Delivered event / gate |
|---|---|---|
message | Inbound message | |
message_echoes | Outbound provider echo message; requires this exact echo gate | |
sent, delivered, read, played | message_status with the corresponding status; exact per-status gate | |
failed | message_status with status: "error"; exact failed gate | |
delete | message_status with status: "deleted"; exact delete gate | |
partner_app_uninstalled, account_disconnected | Same-named event; exact lifecycle subscription | |
messages | message, including supported echoes/reactions/postbacks | |
messaging_seen | message_status with status: "read" | |
comment_automation | comment_automation; exact subscription | |
| Messenger | messages | Inbound/reaction message; exact message gate |
| Messenger | message_echoes | Outbound echo message; exact echo gate |
| Messenger | message_deliveries | message_status with status: "delivered"; exact gate |
| Messenger | message_reads | message_status with status: "read"; exact gate |
| Telegram, SMS, Web Chatt | message | Supported normalized messages |
| Any applicable channel | flag_for_review, session_transfer, end_chat | Same-named operational event |
| Any applicable channel | The seven appointment_* names below | Same-named appointment event |
Generic message aliases (message, messages, message_echoes) and status aliases may match on paths without an exact gate. Do not depend on aliases where an exact gate is stated above: message_status alone does not subscribe a WhatsApp webhook to sent/delivered/read/played/failed/delete, nor a Messenger webhook to deliveries/reads. Subscribe explicitly to the supported provider names. Messenger postbacks use the separate messaging_postbacks gate on their path; support for legacy provider event names outside this catalog is not guaranteed.
Audio transcription is asynchronous: message_status with audio_transcription uses the coarse status alias group (no dedicated transcription checkbox). A status subscription such as sent or delivered, or message_status, matches that path. WhatsApp provider enqueued is stored but does not itself emit a status webhook; acceptance is not proof of delivery.
message envelope
Messages use the following shape. Field names and normalized types are case-sensitive.
{
"eventType": "message",
"timestamp": "2026-09-08T14:14:00.000Z",
"expiresAt": "2026-09-09T14:14:00.000Z",
"payload": {
"provider": "whatsapp",
"direction": "IN",
"timestamp": 1788876840000,
"sessionExpiresAt": "2026-09-09T14:14:00.000Z",
"message": {
"id": "abc123de-f456-489a-bcde-f01234567890",
"sessionId": "7a4b7555-50fa-421e-8eba-d34ac97066b9",
"type": "Text",
"sourceId": "6570618339781",
"ctwaClid": "ARAHuTtGzmFlOO_WcXezfYBRzrxoH2E",
"content": {"text": "Hello"}
},
"sender": {"id": "5511993986082"},
"recipient": {
"id": "5511912345678",
"applicationChannelId": "6cbd6789-1d2b-47e0-9391-5eb91d2839ea"
}
}
}
| Field | Meaning / optionality |
|---|---|
timestamp | Envelope time, ISO 8601 UTC |
expiresAt | Optional session expiry, ISO 8601; omitted when unavailable |
payload.timestamp | Optional original message time, Unix milliseconds; distinct from envelope time |
payload.sessionExpiresAt | Optional session expiry; do not assume it exists before the first inbound |
payload.provider | whatsapp, instagram, messenger, telegram, sms, web_chatt2me |
payload.direction | IN or OUT on current normalized paths |
payload.message.id | Hub message UUID for correlation/reply; distinct from a provider ID |
payload.message.sessionId | Session created/reused before a current inbound/outbound webhook, including Sandbox |
payload.message.type | Persisted type such as Text, Image, Audio, Video, File, Sticker, Location, Contact, Poll, Reaction, Button, List, Reels, Template, QuickReply, Card, Carousel or Read; legacy paths may preserve provider strings |
payload.message.is_echo, is_bot, is_sandbox, is_automated | Sparse optional flags; absence does not guarantee a literal false field |
payload.message.sourceId | Optional opaque Meta ad/post ID from WhatsApp inbound referral.source_id; omitted when absent, empty or malformed |
payload.message.ctwaClid | Optional opaque Click-to-WhatsApp click ID from WhatsApp inbound referral.ctwa_clid; omitted when absent, empty or malformed |
payload.message.isEdited | Optional true on edited inbound messages |
payload.sender, payload.recipient | Routing IDs, with optional available name, username, profilePic; channel participant includes applicationChannelId |
For IN, sender is the visitor and recipient is the channel; reply using recipient.applicationChannelId as from and sender.id as to. For OUT, sender is the channel and recipient is the visitor:
{
"eventType": "message",
"timestamp": "2026-09-08T14:15:00.000Z",
"payload": {
"provider": "whatsapp",
"direction": "OUT",
"message": {
"id": "2d07a67f-d410-46a4-95b0-7b1952a1fcfa",
"sessionId": "7a4b7555-50fa-421e-8eba-d34ac97066b9",
"type": "Text",
"is_echo": true,
"content": {"text": "Welcome"}
},
"sender": {"id": "5511912345678", "applicationChannelId": "6cbd6789-1d2b-47e0-9391-5eb91d2839ea"},
"recipient": {"id": "5511993986082"}
}
}
Outbound content is the persisted outbound content, not necessarily the inbound normalization shown below. Avoid auto-reply loops: process direction: "IN" and filter the provider.
Repeated Meta v3 WhatsApp inbound deliveries with the same original provider
message ID on the same organization and channel do not create another message,
message charge or message event, including concurrent deliveries or changed
intermediary IDs. Later distinct events in the same provider payload still run.
Customer webhook retries remain possible: deduplicate by payload.message.id.
Normalized inbound content examples
These are message fragments to read inside payload.message of the envelope above, not send-message requests. Optional media metadata includes mimeType, fileName, size (bytes), mediaUrls and mediaAttachments[{url,mimeType,fileName}]. Media availability depends on channel and storage processing; do not require fields that are absent.
[
{"type": "Image", "content": {"text": "Photo caption", "mediaUrl": "https://media.example/photo.jpg", "mimeType": "image/jpeg", "fileName": "photo.jpg", "size": 12345}},
{"type": "Audio", "content": {"mediaUrl": "https://media.example/voice.ogg", "mimeType": "audio/ogg"}},
{"type": "Video", "content": {"mediaUrl": "https://media.example/clip.mp4", "mimeType": "video/mp4"}},
{"type": "File", "content": {"mediaUrl": "https://media.example/invoice.pdf", "mimeType": "application/pdf", "fileName": "invoice.pdf"}},
{"type": "Sticker", "content": {"mediaUrl": "https://media.example/sticker.webp", "mimeType": "image/webp"}}
]
[
{"type": "Reels", "content": {"shareType": "ig_reel", "url": "https://www.instagram.com/reel/EXAMPLE/"}},
{"type": "Image", "content": {"shareType": "ig_post", "mediaUrl": "https://media.example/post-thumbnail.jpg"}},
{"type": "Image", "content": {"shareType": "ig_mention", "mediaUrl": "https://media.example/mention.jpg"}}
]
Reel shares use the original page url, not a guaranteed downloadable video/thumbnail. Post shares preserve the image mediaUrl; Meta does not supply the original post page URL on this path.
[
{"type": "Location", "content": {"location": {"latitude": -23.5505, "longitude": -46.6333, "name": "Meeting point", "address": "Example address"}}},
{"type": "Contact", "content": {"contact": {"phoneNumber": "5511999999999", "firstName": "Ana", "formattedName": "Ana Silva", "emails": [{"email": "[email protected]", "type": "WORK"}]}}},
{"type": "Contact", "content": {"contacts": [{"name": {"formatted_name": "Ana Silva", "first_name": "Ana"}, "phones": [{"phone": "5511999999999", "type": "CELL"}]}]}}
]
contact is the normalized single-contact branch; contacts preserves the provider's multi-contact array. Available optional single-contact fields include last/middle name, prefix, suffix, birthday, phones, emails, addresses, URLs and organization.
[
{"type": "Poll", "content": {"poll": {"id": "provider-poll-id", "question": "Choose a time", "options": [{"text": "Morning", "voter_count": 1}, {"text": "Afternoon", "voter_count": 0}], "totalVoterCount": 1, "isClosed": false, "isAnonymous": true, "allowsMultipleAnswers": false, "type": "regular"}}},
{"type": "Reaction", "content": {"reaction": {"messageId": "abc123de-f456-489a-bcde-f01234567890", "action": "add", "emoji": "👍"}}},
{"type": "Button", "content": {"buttons": [{"id": "confirm", "label": "Confirm", "type": "reply"}]}},
{"type": "Button", "content": {"buttons": [{"id": "slot-1", "label": "Morning", "type": "list"}]}},
{"type": "Button", "content": {"buttons": [{"id": "catalog", "label": "View catalog", "type": "postback"}]}}
]
Telegram lists become polls with at least two options. The webhook exposes poll ID and option text; do not expect original list row IDs or selected option indices. Reaction action and emoji are optional; removal may omit emoji.
[
{"type": "Text", "content": {"text": "That one", "context": {"id": "abc123de-f456-489a-bcde-f01234567890"}}},
{"type": "Text", "content": {"deleted": true}},
{"type": "Text", "content": {"text": "Shared note", "forwarded": true, "frequentlyForwarded": true}},
{"type": "Text", "isEdited": true, "content": {"text": "Corrected note"}}
]
Context IDs identify the referenced message and can originate from provider reply metadata; correlate with stored messages rather than assuming every ID is a Hub UUID. The message envelope's own message.id is the Hub ID. WhatsApp revoke notifications use the separate message_status: deleted path below.
Instagram/Messenger multi-image input emits one message/event/Hub ID per image, sharing session and original provider time. Only the first preserves caption/reply context. Deduplicating by session or timestamp discards attachments; use individual message IDs.
Creating a session does not itself charge a channel conversation. Billable outbound messages are forwarded only after the applicable charge can be fully funded. Provider echoes that cannot be funded are retained for audit but are not forwarded to customer webhooks. A valid AI result finalizes its AI service charge even if delivery later fails; conversation/channel outbound charges remain tied to delivery.
comment_automation event
Subscribe an Instagram application channel to comment_automation to receive
the private reply produced by a matching comment rule. The Hub emits this
event once after Meta accepts the reply and the message is persisted. It is
not also emitted as a message event.
{
"eventType": "comment_automation",
"timestamp": "2026-09-14T15:00:00.000Z",
"payload": {
"provider": "instagram",
"direction": "OUT",
"message": {
"id": "2d07a67f-d410-46a4-95b0-7b1952a1fcfa",
"sessionId": "3892f9da-202f-4b90-9d4d-85c779211b8e",
"type": "Text",
"is_automated": true,
"content": { "text": "Here is the link you requested" }
},
"sender": {
"id": "@bakery_business",
"name": "Bakery Business",
"username": "bakery_business",
"applicationChannelId": "7c37a280-708c-46d5-b1fe-8c20a372df32"
},
"recipient": {
"id": "987654321",
"name": "Ana Silva",
"username": "ana"
}
}
}
The event always has message.is_automated: true and a real sessionId.
Before the recipient's first inbound response, expiresAt and
payload.sessionExpiresAt are absent. Sender and recipient include only safe
fields available from the connected account and commenter; unavailable fields
are omitted. No raw Meta payload or credentials are included.
message_status event
This is a flat envelope, not payload.message, and has no channel field. timestamp is ISO 8601 UTC. Possible statuses are queued, sent, delivered, read, played, error, audio_transcription, and deleted; availability depends on the emitting path/provider. failed is a subscription/provider status mapped to normalized error.
Optional fields are messageId (Hub ID), providerMessageId, errorMessage, errorCode (string), errorDetails, transcription, expiresAt and Sandbox is_sandbox. Do not reject status events solely because a Hub message could not be correlated.
{"eventType": "message_status", "messageId": "9307b9ab-5fab-4151-b7c2-b00c2a8a6e8e", "providerMessageId": "provider-id", "status": "delivered", "timestamp": "2026-09-08T14:15:00.000Z"}
{"eventType": "message_status", "messageId": "9307b9ab-5fab-4151-b7c2-b00c2a8a6e8e", "status": "error", "timestamp": "2026-09-08T14:15:00.000Z", "errorCode": "131026", "errorMessage": "Message undeliverable", "errorDetails": "Provider could not deliver the message"}
{"eventType": "message_status", "messageId": "9307b9ab-5fab-4151-b7c2-b00c2a8a6e8e", "status": "audio_transcription", "timestamp": "2026-09-08T14:16:00.000Z", "transcription": "Please call me tomorrow"}
{"eventType": "message_status", "providerMessageId": "provider-revoke-id", "status": "deleted", "timestamp": "2026-09-08T14:17:00.000Z"}
Agent operational events
flag_for_review, session_transfer, and end_chat use this envelope:
{
"eventType": "session_transfer",
"timestamp": "2026-08-19T13:06:34.806Z",
"expiresAt": "2026-08-20T13:14:34.906Z",
"payload": {
"provider": "web_chatt2me",
"direction": "IN",
"message": {
"id": "f3e1c5d0-4b8a-4e2b-9f3c-1a2b3c4d5e6f",
"sessionId": "7a4b7555-50fa-421e-8eba-d34ac97066b9",
"queue": { "id": "28fb06bc-ce0d-4e3b-8446-50bd47b25818", "name": "Financeiro" },
"reason": "requested_by_visitor",
"summary": "Visitor requested financial support."
},
"sender": {
"id": "visitor-id",
"name": "Fernando",
"username": "Fernando",
"phoneNumber": "5511999999999",
},
"recipient": { "id": "chatt2me", "applicationChannelId": "35c32d6a-cc9a-4b1d-8950-682802767cc5" }
}
}
payload.message.id is a newly generated Hub message ID and can be used to
correlate logs and retries. expiresAt is present when the session has an
expiry. flag_for_review contains sessionId and summary; end_chat adds
reason and a required natural-language summary; session_transfer can also include queue.
An automatic inactivity closure emits end_chat with
payload.message.reason: "inactivity". If its notice was delivered, that
notice also appears as a subscribed outbound message event with
payload.message.is_bot: true.
When the Agent uses summary_on_transfer, active-session message/status forwarding (including follow-up messages) can be suppressed; operational end_chat remains a separate subscribed event.
For Web Chatt inbound messages and Web Chatt operational events, sender can
also include optional phoneNumber (digits without +) and email. These
values are visitor-provided and unverified; they are not identity or routing
keys. Continue to use sender.id when replying to the visitor. Other channel
providers and Web Chatt outbound recipients do not include these contact fields.
Appointment events
Subscribe individually to the exact event names on the appointment's associated channel. All seven use payload.appointment; events require a linked channel and visitor.
| Event | Trigger |
|---|---|
appointment_action_required | Booking requires approval, or takeover requests human action |
appointment_confirmed | Booking confirmed |
appointment_alternatives_requested | Alternatives requested |
appointment_rescheduled | Booking rescheduled/updated |
appointment_cancelled | Confirmed booking cancelled |
appointment_rejected | Pending proposal rejected |
appointment_expired | Pending proposal expired |
{
"eventType": "appointment_action_required",
"timestamp": "2026-10-06T12:00:00.000Z",
"payload": {
"appointment": {
"id": "4e52f12d-d0a2-48ef-b172-8cb97dad95cc",
"status": "pending_confirmation",
"service": {"id": "ce45810e-165c-4e23-885d-a4f6e1de45ce", "name": "Consultation"},
"start": "2026-10-07T13:00:00.000Z",
"end": "2026-10-07T13:30:00.000Z",
"timezone": "America/Sao_Paulo",
"location": "Office",
"reason": null,
"version": 1
}
}
}
start/end are ISO instants or null; timezone describes local scheduling. Location/reason can be null. version supports correlation with appointment mutations. See Calendar booking for statuses and actions.
WhatsApp disconnection events
partner_app_uninstalled is emitted for provider partner-app removal; account_disconnected for provider account disconnection. They require a matching connected channel and the exact subscription. Both share this payload:
{
"eventType": "account_disconnected",
"timestamp": "2026-10-06T12:00:00.000Z",
"payload": {
"provider": "whatsapp",
"applicationChannelId": "6cbd6789-1d2b-47e0-9391-5eb91d2839ea",
"phoneNumber": "5511912345678",
"gsAppId": "provider-app-id",
"wabaId": null,
"providerEventId": "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c",
"occurredAt": 1791288000,
"reason": "account_disconnected",
"initiatedBy": null
}
}
phoneNumber, wabaId, occurredAt, reason and initiatedBy may be null when absent from the provider; occurredAt preserves provider Unix seconds, unlike envelope ISO time. Correlate by channel and providerEventId; optional provider IDs are identifiers, not credentials. Do not assume disconnect reason/initiator values form a fixed enum.
Reply processing after durable acceptance
Use the receiver to authenticate/validate and persist before 2xx. Have your worker load an accepted event, process it, and mark it complete only after success. The following function is WhatsApp-only; call it from that worker, not before durable acceptance or after an unrecorded early acknowledgement:
async function replyToWhatsApp(event) {
const p = event.payload;
if (event.eventType !== 'message' || p?.provider !== 'whatsapp' ||
p.direction !== 'IN' || p.message?.content?.text !== 'hi') return;
const response = await fetch('https://app.chatt2.me/v1/communication/whatsapp/message', {
method: 'POST',
headers: { 'x-api-key': process.env.CHATT2ME_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ from: p.recipient.applicationChannelId, to: p.sender.id,
content: { text: 'Hello! How can I help you?' } }),
});
if (!response.ok) throw new Error(`Hub reply failed: ${response.status}`);
const result = await response.json();
if (result.status === 'error') throw new Error('Provider rejected the reply');
return result;
}
Retain and retry failed worker jobs yourself; use deduplication to avoid repeat replies after ambiguous failures. Hub retry is for eligible failed deliveries, not replaying successful acknowledgements.