Skip to main content
Version: 1.0

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​

ProviderSubscribe toDelivered event / gate
WhatsAppmessageInbound message
WhatsAppmessage_echoesOutbound provider echo message; requires this exact echo gate
WhatsAppsent, delivered, read, playedmessage_status with the corresponding status; exact per-status gate
WhatsAppfailedmessage_status with status: "error"; exact failed gate
WhatsAppdeletemessage_status with status: "deleted"; exact delete gate
WhatsApppartner_app_uninstalled, account_disconnectedSame-named event; exact lifecycle subscription
Instagrammessagesmessage, including supported echoes/reactions/postbacks
Instagrammessaging_seenmessage_status with status: "read"
Instagramcomment_automationcomment_automation; exact subscription
MessengermessagesInbound/reaction message; exact message gate
Messengermessage_echoesOutbound echo message; exact echo gate
Messengermessage_deliveriesmessage_status with status: "delivered"; exact gate
Messengermessage_readsmessage_status with status: "read"; exact gate
Telegram, SMS, Web ChattmessageSupported normalized messages
Any applicable channelflag_for_review, session_transfer, end_chatSame-named operational event
Any applicable channelThe seven appointment_* names belowSame-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"
}
}
}
FieldMeaning / optionality
timestampEnvelope time, ISO 8601 UTC
expiresAtOptional session expiry, ISO 8601; omitted when unavailable
payload.timestampOptional original message time, Unix milliseconds; distinct from envelope time
payload.sessionExpiresAtOptional session expiry; do not assume it exists before the first inbound
payload.providerwhatsapp, instagram, messenger, telegram, sms, web_chatt2me
payload.directionIN or OUT on current normalized paths
payload.message.idHub message UUID for correlation/reply; distinct from a provider ID
payload.message.sessionIdSession created/reused before a current inbound/outbound webhook, including Sandbox
payload.message.typePersisted 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_automatedSparse optional flags; absence does not guarantee a literal false field
payload.message.sourceIdOptional opaque Meta ad/post ID from WhatsApp inbound referral.source_id; omitted when absent, empty or malformed
payload.message.ctwaClidOptional opaque Click-to-WhatsApp click ID from WhatsApp inbound referral.ctwa_clid; omitted when absent, empty or malformed
payload.message.isEditedOptional true on edited inbound messages
payload.sender, payload.recipientRouting 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.

Media message fragments
[
{"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"}}
]
Instagram share fragments
[
{"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.

Location and contacts fragments
[
{"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.

Poll, reaction and button fragments
[
{"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.

Reply, deletion, forwarding and edit fragments
[
{"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.

Delivered
{"eventType": "message_status", "messageId": "9307b9ab-5fab-4151-b7c2-b00c2a8a6e8e", "providerMessageId": "provider-id", "status": "delivered", "timestamp": "2026-09-08T14:15:00.000Z"}
Delivery error
{"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"}
Transcription ready
{"eventType": "message_status", "messageId": "9307b9ab-5fab-4151-b7c2-b00c2a8a6e8e", "status": "audio_transcription", "timestamp": "2026-09-08T14:16:00.000Z", "transcription": "Please call me tomorrow"}
Deletion without a matched Hub ID
{"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",
"email": "[email protected]"
},
"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.

EventTrigger
appointment_action_requiredBooking requires approval, or takeover requests human action
appointment_confirmedBooking confirmed
appointment_alternatives_requestedAlternatives requested
appointment_rescheduledBooking rescheduled/updated
appointment_cancelledConfirmed booking cancelled
appointment_rejectedPending proposal rejected
appointment_expiredPending 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:

WhatsApp-only worker function
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.