Skip to main content

Webhook Events Reference

This page lists all available webhook events, their triggers, and payload formats. Use this as a reference when building your webhook handler.

Payload Envelope

Every webhook delivery includes a standard envelope with event-specific data in the data field:

{
"id": "evt_abc123def456",
"type": "message.received",
"event_id": "evt_abc123def456",
"created_at": "2026-03-04T10:30:00Z",
"tenant_id": "tenant_abc123",
"data": {
// Event-specific fields
}
}
FieldTypeDescription
idstringUnique event ID (for deduplication)
typestringEvent type (e.g., message.received)
event_idstringAlias for id, included for convenience
created_atstringISO 8601 timestamp
tenant_idstringYour tenant ID
dataobjectEvent-specific payload

Message Events

All message events (message.received, message.sent, message.delivered, message.read, message.failed) share the same enriched payload structure. The data object always contains the sub-objects message, conversation, and contact (the contact includes its contact_methods[]), plus a top-level contact_method object identifying the single contact method used for that message. This gives your webhook handler complete context without requiring follow-up API calls. (The examples for message.delivered/message.read/message.failed below abbreviate contact and omit contact_method for brevity — at runtime they carry the same enriched shape as message.sent.)

tip

For detailed payload examples of every message type (images, videos, location pins, contact cards, interactive replies, reactions, templates, carousels, and more), see the Message Types Reference.

message.received

Triggered when a new inbound message arrives from a contact.

{
"id": "evt_msg_001",
"type": "message.received",
"event_id": "evt_msg_001",
"created_at": "2026-03-04T10:30:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message": {
"id": "msg_abc123",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "inbound",
"message_type": "image",
"text": null,
"attachments": [
{
"id": "att_abc123",
"type": "image",
"filename": "whatsapp_image_abc123.jpg",
"content_type": "image/jpeg",
"file_size": 245000,
"url": "https://api.sendseven.com/api/v1/attachments/att_abc123/download",
"signed_url": "https://storage.googleapis.com/sendseven-attachments/tenant_abc123/att_abc123.jpg?X-Goog-Signature=..."
}
],
"status": "received",
"from_id": "+1234567890",
"to_id": null,
"external_id": "wamid.abc123",
"reply_to_external_id": null,
"reply_to_id": null,
"meta": {},
"created_at": "2026-03-04T10:30:00Z",
"sent_at": null,
"delivered_at": null,
"read_at": null
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": [
{
"id": "cm_789",
"method_type": "whatsapp_id",
"value": "1234567890",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
},
{
"id": "cm_790",
"method_type": "phone",
"value": "+1234567890",
"channel_id": null,
"display_name": null,
"phone_type": "mobile",
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
]
},
"contact_method": {
"id": "cm_789",
"method_type": "whatsapp_id",
"value": "1234567890",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
}
}

message.sent

Triggered when an outbound message is sent to the channel provider.

{
"id": "evt_msg_002",
"type": "message.sent",
"event_id": "evt_msg_002",
"created_at": "2026-03-04T10:35:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message": {
"id": "msg_def456",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "outbound",
"message_type": "document",
"text": "Here's your invoice",
"attachments": [
{
"id": "att_def456",
"type": "document",
"filename": "invoice_march_2026.pdf",
"content_type": "application/pdf",
"file_size": 184320,
"url": "https://api.sendseven.com/api/v1/attachments/att_def456/download",
"signed_url": "https://storage.googleapis.com/sendseven-attachments/tenant_abc123/att_def456.pdf?X-Goog-Signature=..."
}
],
"status": "sent",
"from_id": "+1987654321",
"to_id": "+1234567890",
"external_id": "wamid.def456",
"meta": {},
"created_at": "2026-03-04T10:35:00Z",
"sent_at": "2026-03-04T10:35:01Z",
"delivered_at": null,
"read_at": null
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": [
{
"id": "cm_789",
"method_type": "whatsapp_id",
"value": "1234567890",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": null,
"last_used_at": "2026-03-04T10:35:01Z",
"created_at": "2026-03-04T10:29:00Z"
},
{
"id": "cm_790",
"method_type": "phone",
"value": "+1234567890",
"channel_id": null,
"display_name": null,
"phone_type": "mobile",
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
]
},
"contact_method": {
"id": "cm_789",
"method_type": "whatsapp_id",
"value": "1234567890",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": null,
"last_used_at": "2026-03-04T10:35:01Z",
"created_at": "2026-03-04T10:29:00Z"
}
}
}

message.delivered

Triggered when delivery is confirmed by the channel.

{
"id": "evt_msg_003",
"type": "message.delivered",
"event_id": "evt_msg_003",
"created_at": "2026-03-04T10:35:05Z",
"tenant_id": "tenant_abc123",
"data": {
"message": {
"id": "msg_def456",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "outbound",
"message_type": "text",
"text": "Hi John! Let me look into your order right away.",
"status": "delivered",
"from_id": "+1987654321",
"to_id": "+1234567890",
"external_id": "wamid.def456",
"meta": {},
"created_at": "2026-03-04T10:35:00Z",
"sent_at": "2026-03-04T10:35:01Z",
"delivered_at": "2026-03-04T10:35:05Z",
"read_at": null
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890"
}
}
}

message.read

Triggered when a read receipt is received from the channel, confirming the contact has seen the message.

{
"id": "evt_msg_004",
"type": "message.read",
"event_id": "evt_msg_004",
"created_at": "2026-03-04T10:36:12Z",
"tenant_id": "tenant_abc123",
"data": {
"message": {
"id": "msg_def456",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "outbound",
"message_type": "text",
"text": "Hi John! Let me look into your order right away.",
"status": "read",
"from_id": "+1987654321",
"to_id": "+1234567890",
"external_id": "wamid.def456",
"meta": {},
"created_at": "2026-03-04T10:35:00Z",
"sent_at": "2026-03-04T10:35:01Z",
"delivered_at": "2026-03-04T10:35:05Z",
"read_at": "2026-03-04T10:36:12Z"
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890"
}
}
}

message.reaction

Triggered when an emoji reaction is added to or removed from a message. The data.reaction.action field is "added" or "removed", so a single subscription covers the full reaction lifecycle.

This event fires for reactions on both directions of a conversation — a contact reacting to one of your messages (inbound) and an agent reacting to a contact's message (outbound) — on every channel that supports reactions: WhatsApp, Telegram, Messenger, Instagram, and Live Chat. The data.message object is the target message the reaction is attached to.

The data.reaction object has this shape:

FieldTypeDescription
emojistringThe reaction emoji. For a removal the platform may send an empty string.
actionstring"added" or "removed".
from_idstring | nullPlatform id of who reacted (the contact for inbound reactions; the agent/user id or null for outbound).
{
"id": "evt_msg_006",
"type": "message.reaction",
"event_id": "evt_msg_006",
"created_at": "2026-03-04T10:37:00Z",
"tenant_id": "tenant_abc123",
"data": {
"reaction": {
"emoji": "👍",
"action": "added",
"from_id": "+1234567890"
},
"message": {
"id": "msg_def456",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "outbound",
"message_type": "text",
"text": "Hi John! Let me look into your order right away.",
"status": "read",
"from_id": "+1987654321",
"to_id": "+1234567890",
"external_id": "wamid.def456",
"meta": {},
"created_at": "2026-03-04T10:35:00Z"
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890"
}
}
}

Reactions are free. A reaction is never counted as a billable message — it does not appear in usage or on your invoice.

message.failed

Triggered when message delivery fails.

{
"id": "evt_msg_005",
"type": "message.failed",
"event_id": "evt_msg_005",
"created_at": "2026-03-04T10:35:10Z",
"tenant_id": "tenant_abc123",
"data": {
"message": {
"id": "msg_ghi789",
"conversation_id": "conv_xyz789",
"platform": "whatsapp",
"channel_id": "ch_123",
"contact_id": "ct_456",
"contact_method_id": "cm_789",
"direction": "outbound",
"message_type": "text",
"text": "Your order has been shipped!",
"status": "failed",
"from_id": "+1987654321",
"to_id": "+1234567890",
"external_id": null,
"meta": {
"error": "Message undeliverable",
"error_code": "131047"
},
"created_at": "2026-03-04T10:35:08Z",
"sent_at": null,
"delivered_at": null,
"read_at": null
},
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"created_at": "2026-03-04T10:29:00Z"
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890"
}
}
}

Message Fields Reference

FieldTypeDescription
idstringUnique message ID
conversation_idstringParent conversation ID
platformstringChannel platform (whatsapp, telegram, sms, messenger, instagram, email)
channel_idstringChannel configuration ID
contact_idstringContact ID
contact_method_idstring | nullFK to the contact method used for delivery. The full method object is delivered at the top level as data.contact_method (see Contact Method Fields Reference).
directionstringinbound or outbound
message_typestringtext, image, video, audio, document, sticker, location, template, button, interactive
textstring | nullMessage text content
statusstringreceived, sent, delivered, read, failed
from_idstringSender identifier (phone number, platform ID)
to_idstring | nullRecipient identifier
external_idstring | nullProvider message ID (e.g., WhatsApp wamid)
reply_to_external_idstring | nullInbound. Platform message ID of the message this one quotes/replies to, or — for a tapped quick-reply / interactive button — the message that carried the button. null when the message is not a reply. Normalized identically across WhatsApp, Telegram, Instagram and Messenger. See Reply / interaction context.
reply_to_idstring | nullInbound. SendSeven message id of the replied-to message — reply_to_external_id resolved to our internal ID (best-effort, same conversation). null when the message is not a reply or the quoted message isn't stored (e.g. it predates the conversation or was never delivered to us). Lets you correlate a reply to the original without an extra lookup.
metaobjectAdditional metadata (always a JSON object). On inbound messages it carries channel-parsed context (e.g. meta.button for button/list clicks); on outbound it carries the sender-supplied metadata (e.g. flow_run_id, template/interactive details); on message.failed it contains error and error_code. Contents differ by direction and message type.
contact_infoobject | nullInbound only, deprecated. Raw sender-profile snapshot echoed by the channel provider at receipt time. Absent on outbound. See the caution note below — prefer data.contact_method and data.contact.
created_atstringISO 8601 creation timestamp
sent_atstring | nullISO 8601 sent timestamp
delivered_atstring | nullISO 8601 delivery confirmation timestamp
attachmentsarray | nullFile attachments with download URLs (see Attachments below)
read_atstring | nullISO 8601 read receipt timestamp
Deprecated: contact_info

Inbound message.received payloads may also carry an undocumented contact_info object — a raw snapshot of the sender's profile echoed by the channel provider at receipt time. Its phone field (WhatsApp only) is deprecated and no longer documented: it is a provider echo, not an authoritative value. Use data.contact_method.value for the identifier of the method actually used, and the data.contact record (phone, contact_methods[]) for the contact's stored identifiers. Do not build new integrations against contact_info.

Attachments

When a message includes file-based attachments (images, videos, audio, documents, stickers), the attachments array is included in the message object and each attachment provides two download URLs. The table below describes this enriched file shape only — non-file attachment types (location, contacts, reactions, button/list replies, flow responses) also live in attachments but use different, type-specific fields (see Message Types Reference):

FieldTypeDescription
idstringUnique attachment ID
typestringAttachment type: image, video, audio, document, sticker, file
filenamestringOriginal or generated filename
content_typestringMIME type (e.g., image/jpeg, application/pdf)
file_sizeinteger | nullFile size in bytes
urlstringStable API download URL — requires authentication, never expires
signed_urlstringPre-signed GCS URL — no authentication required, expires in 24 hours
Use url for reliable access

The url field points to the SendSeven API (/api/v1/attachments/{id}/download) and works as long as the attachment exists. Use this for permanent storage or delayed processing. The signed_url is convenient for immediate one-time downloads without authentication but expires after 24 hours.

Non-file attachments (location, contacts, reactions, button/list replies, flow responses) do appear in the attachments array — each in its own type-specific shape, not the file download shape above (they have no url/signed_url/file_size). For example, a button reply is { "type": "button_reply", "button_id": "...", "button_title": "..." } and a location is { "type": "location", "latitude": ..., "longitude": ..., "name": "...", "address": "..." }. Button, list, and quick-reply clicks are additionally surfaced as a structured meta.button object ({ id, payload, text }) for convenience. See the Message Types Reference for the exact shape of every non-file attachment type.

Reply / interaction context (cross-channel): when an inbound message references another message — the customer quoted/replied to a specific message, or tapped a quick-reply / interactive button — the reference is surfaced as two flat, top-level fields on the message.received payload, identical in shape on every channel:

  • reply_to_external_id — the platform message ID of the message being replied to (or the message a tapped button belonged to). This is the raw provider identifier, matching the external_id the provider gave that original message.
  • reply_to_id — the SendSeven message id that reply_to_external_id resolved to, so you can link straight to the original message in your own copy of the conversation. Best-effort: it is null when we don't have the quoted message stored (it predates the conversation, was never delivered to us, or is an outbound we sent). reply_to_external_id is still populated in that case.

Both fields are null when the message is not a reply. Rely on these two fields for reply correlation on all channels — you do not need to read anything channel-specific.

Per-channel source of reply_to_external_id:

ChannelSource
WhatsAppcontext.id (quoted reply and quick-reply / interactive button taps)
Facebook Messengerreply_to.mid
Instagramreply_to.mid
Telegramreply_to_message.message_id; inline-button taps use the originating message ID

Email uses standard In-Reply-To / References header threading on its own email.received events and does not populate these fields.

Channel-specific extras (meta.context). A few of WhatsApp's context details have no cross-channel equivalent, so they remain under a WhatsApp-only meta.context object (present only when supplied): from (sender of the quoted message), forwarded / frequently_forwarded (booleans), and referred_product ({ catalog_id, product_retailer_id }). The reply identity is not duplicated here — use the flat reply_to_external_id / reply_to_id fields for that. Instagram story replies keep their story details under meta.story_reply ({ story_id, media_url }, media_url is a short-lived Meta CDN link ~24 h).

Fallback behavior: If SendSeven could not download the media from the channel platform (e.g., expired WhatsApp media URL), the raw platform data is included with "source": "platform" added to each attachment object instead of the enriched fields.

Conversation Fields Reference

FieldTypeDescription
idstringConversation ID
channel_idstringChannel configuration ID
contact_idstring | nullContact ID
assigned_user_idstring | nullUser the conversation is assigned to, if any.
bot_session_idstring | nullActive bot/automation session ID, if the conversation is being handled by a bot.
statusstringopen, closed, pending
subjectstring | nullConversation subject
last_customer_message_atstring | nullISO 8601 timestamp of the most recent inbound customer message.
last_agent_reply_atstring | nullISO 8601 timestamp of the most recent agent reply.
snoozed_untilstring | nullISO 8601 UTC timestamp the conversation is snoozed until. When set and in the future, the conversation is "snoozed" (still status: open) and hidden from the Open tab. null when not snoozed. Present on conversation.updated (snooze); not emitted in the message-event conversation sub-object. See Snooze.
snooze_reopen_on_messageboolean | nullWhether an incoming customer message auto-reopens the snoozed conversation. null when not snoozed.
created_atstringISO 8601 creation timestamp

Contact Fields Reference

FieldTypeDescription
idstringContact ID
namestring | nullContact display name
emailstring | nullContact email address (denormalized from primary email method)
phonestring | nullContact phone number (denormalized from primary phone method)
contact_methodsarrayAll of the contact's identifiers (WhatsApp ID, phone, email, Telegram ID, etc.), primary method first. See Contact Method Fields Reference.

The contact_methods[] array is included on every event that carries a contact object (message.received, message.sent, message.delivered, message.read, message.failed, conversation.created, conversation.assigned, contact.created, contact.updated, link.clicked). The denormalized top-level phone/email fields are retained for backwards compatibility. Technical, internal-only method types (e.g. live-chat visitor cookies) are never included.

Contact Method Fields Reference

The top-level contact_method object (a sibling of message, conversation, and contact) is included on all message events (message.received, message.sent, message.delivered, message.read, message.failed). It identifies the single contact method actually used for that message (the recipient/sender identifier). The message object still carries a contact_method_id string (an FK reference); the full method object lives at the top level. Each entry in contact.contact_methods[] uses the same shape.

FieldTypeDescription
idstringContact method ID (matches message.contact_method_id)
method_typestringphone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id, or a social-profile type (linkedin, homepage, facebook, instagram_handle, social_other)
valuestringThe identifier value (phone number, email, platform ID)
channel_idstring | nullChannel ID for channel-scoped methods (whatsapp_bsuid, messenger_id, instagram_id); null for global methods
display_namestring | nullDisplay name associated with the method. For whatsapp_bsuid, the contact's public WhatsApp username when known (a label -- it is not addressable)
phone_typestring | nullFor phone methods: mobile, landline, etc.
is_primarybooleanWhether this is the primary method of its type
email_statusstring | nullFor email methods: validation/deliverability status
last_used_atstring | nullISO 8601 timestamp the method was last used
created_atstringISO 8601 creation timestamp
Treat method_type as an open enum

New method types are added as messaging platforms change. whatsapp_bsuid (a WhatsApp Business-scoped user ID) is the most recent: it is channel-scoped, so it carries a non-null channel_id on a WhatsApp method -- previously only Messenger and Instagram did -- and a contact may have a whatsapp_bsuid and no phone/whatsapp_id at all. Make sure your consumer passes unknown types through rather than dropping the record or failing the delivery. See Migrating to BSUIDs.


Conversation Events

conversation.created

Triggered when a new conversation starts.

{
"id": "evt_conv_001",
"type": "conversation.created",
"event_id": "evt_conv_001",
"created_at": "2026-03-04T10:29:00Z",
"tenant_id": "tenant_abc123",
"data": {
"conversation_id": "conv_xyz789",
"contact_id": "ct_456",
"platform": "whatsapp",
"channel_id": "ch_123",
"status": "open",
"assigned_user_id": null,
"created_at": "2026-03-04T10:29:00Z",
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": [
{
"id": "cm_789",
"method_type": "whatsapp_id",
"value": "1234567890",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
]
}
}
}

conversation.updated

Triggered when a conversation changes without opening or closing — currently when it is snoozed or unsnoozed. The data.change field tells you which transition occurred, and the embedded conversation object carries the current snooze fields. See the Snooze guide for the full feature.

{
"id": "evt_conv_010",
"type": "conversation.updated",
"event_id": "evt_conv_010",
"created_at": "2026-06-22T14:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"change": "snoozed",
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"contact_id": "ct_456",
"status": "open",
"subject": null,
"snoozed_until": "2026-06-23T09:00:00Z",
"snooze_reopen_on_message": true,
"created_at": "2026-06-22T10:29:00Z"
},
"actor": {
"user_id": "user_1a2b3c"
}
}
}
data fieldDescription
changeThe transition: "snoozed" or "unsnoozed".
conversationThe updated conversation object (includes snoozed_until and snooze_reopen_on_message).
actorThe user who triggered the change. Omitted for system-triggered changes such as auto-reopen when a customer replies to a snoozed conversation.
Snooze is a derived state

A snoozed conversation keeps status: "open" — "snoozed" simply means snoozed_until is in the future. When that time passes, the conversation returns to the Open tab automatically and no conversation.updated event is emitted for the lapse (expiry is evaluated at query time). You only receive conversation.updated for explicit snooze/unsnooze actions and for auto-reopen on an inbound message.

conversation.closed

Triggered when a conversation is closed/resolved.

{
"id": "evt_conv_002",
"type": "conversation.closed",
"event_id": "evt_conv_002",
"created_at": "2026-03-04T15:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"conversation_id": "conv_xyz789",
"contact_id": "ct_456",
"platform": "whatsapp",
"channel_id": "ch_123",
"status": "closed",
"assigned_user_id": "user_1a2b3c",
"closed_by": "user_1a2b3c",
"summary": "Order issue resolved",
"created_at": "2026-03-04T10:29:00Z",
"last_customer_message_at": "2026-03-04T14:30:00Z",
"last_agent_reply_at": "2026-03-04T14:45:00Z"
}
}

conversation.transcript.created

Triggered when a transcript export of a conversation has finished generating and is ready to download. Transcript exports are requested via POST /api/v1/conversations/{conversation_id}/transcript (with a format of pdf or zip); the file is generated asynchronously, so this event fires once the artifact has been rendered and uploaded to storage.

{
"id": "evt_transcript_001",
"type": "conversation.transcript.created",
"event_id": "job_9f8e7d6c",
"created_at": "2026-07-02T12:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"conversation_id": "conv_xyz789",
"tenant_id": "tenant_abc123",
"contact_id": "ct_456",
"format": "pdf",
"job_id": "job_9f8e7d6c",
"download_url": "https://storage.googleapis.com/sendseven-exports/tenant_abc123/exports/2026-07-02-1200-John-Doe.pdf?X-Goog-Signature=...",
"download_url_expires_at": "2026-07-02T13:00:00Z",
"download_url_expires_in_minutes": 60,
"poll_url": "/api/v1/conversations/conv_xyz789/transcript/job_9f8e7d6c",
"filename": "2026-07-02-1200-John-Doe.pdf"
}
}
data fieldTypeDescription
conversation_idstringConversation the transcript was exported from.
tenant_idstringYour tenant ID (also on the envelope).
contact_idstring | nullContact on the conversation, if the conversation has one.
formatstringExport format: pdf or zip.
job_idstringThe export job ID. Also used as the event event_id.
download_urlstringPre-signed, short-lived download URL for the artifact (forces download with a friendly filename).
download_url_expires_atstringISO 8601 UTC timestamp when download_url expires.
download_url_expires_in_minutesintegerLifetime of download_url in minutes.
poll_urlstringDurable API path to re-fetch a fresh download URL after the signed URL expires — GET /api/v1/conversations/{conversation_id}/transcript/{job_id}.
filenamestringSuggested download filename.
download_url is short-lived

The download_url is a pre-signed storage URL that expires (see download_url_expires_at). If your webhook handler processes the event after that window — for example after a retry or a queued job — the URL will no longer work. Use poll_url to fetch a fresh download URL on demand instead of persisting download_url.


Contact Events

contact.created

Triggered when a new contact is added.

The contact is nested under data.contact — the same object described in Contact Fields Reference.

{
"id": "evt_contact_001",
"type": "contact.created",
"event_id": "evt_contact_001",
"created_at": "2026-03-04T10:29:00Z",
"tenant_id": "tenant_abc123",
"data": {
"contact": {
"id": "ct_456",
"tenant_id": "tenant_abc123",
"name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+1234567890",
"languages": "en",
"birthday": null,
"source": "api",
"contact_methods": [
{
"id": "cm_790",
"method_type": "phone",
"value": "+1234567890",
"channel_id": null,
"display_name": null,
"phone_type": "mobile",
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
},
{
"id": "cm_791",
"method_type": "email",
"value": "[email protected]",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": "valid",
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
],
"created_at": "2026-03-04T10:29:00Z",
"updated_at": "2026-03-04T10:29:00Z"
}
}
}

contact.updated

Triggered when contact information changes — and when the contact is the survivor of a contact merge (see Merged contacts below).

{
"id": "evt_contact_002",
"type": "contact.updated",
"event_id": "evt_contact_002",
"created_at": "2026-03-04T11:15:00Z",
"tenant_id": "tenant_abc123",
"data": {
"contact": {
"id": "ct_456",
"tenant_id": "tenant_abc123",
"name": "John Doe",
"first_name": "John",
"last_name": "Doe",
"email": "[email protected]",
"phone": "+1234567890",
"languages": "en",
"birthday": null,
"source": "api",
"contact_methods": [
{
"id": "cm_790",
"method_type": "phone",
"value": "+1234567890",
"channel_id": null,
"display_name": null,
"phone_type": "mobile",
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
},
{
"id": "cm_792",
"method_type": "email",
"value": "[email protected]",
"channel_id": null,
"display_name": null,
"phone_type": null,
"is_primary": true,
"email_status": "valid",
"last_used_at": null,
"created_at": "2026-03-04T11:15:00Z"
}
],
"created_at": "2026-03-04T10:29:00Z",
"updated_at": "2026-03-04T11:15:00Z"
}
}
}

Merged contacts

Two contact records can collapse into one — when an agent merges duplicates by hand, when a visitor's email and phone turn out to belong to two existing records, or when WhatsApp finally discloses the phone number of someone you only knew by their Business-scoped user ID. When that happens SendSeven sends one contact.updated for the surviving contact with an extra merge object beside contact:

{
"id": "evt_contact_003",
"type": "contact.updated",
"event_id": "evt_contact_003",
"created_at": "2026-03-04T12:02:11Z",
"tenant_id": "tenant_abc123",
"data": {
"contact": {
"id": "ct_456",
"name": "John Doe",
"phone": "+1234567890",
"email": "[email protected]",
"contact_methods": []
},
"merge": {
"merged_from": ["ct_991", "ct_992"],
"merged_at": "2026-03-04T12:02:11Z",
"reason": "auto"
}
}
}
FieldTypeDescription
merged_fromarray of stringThe contact IDs that were absorbed. These IDs no longer identify a separate contact. Usually one entry; a single merge can absorb several.
merged_atstring (ISO 8601)When the merge happened.
reasonstringWhat triggered it: manual (an agent merged them), auto (identifiers resolved to the same person), import, live_chat, api.

How to reconcile. For every ID in merged_from, re-point your own records at data.contact.id and then treat the old ID as an alias, not as a deleted record:

  1. Update your stored mapping so the absorbed IDs now resolve to data.contact.id.
  2. Re-read data.contact.contact_methods in full and replace your copy. A merge can add identifiers (the survivor inherits the other record's phone, email and platform IDs), drop duplicates, and change which method is is_primary — so treat the array as authoritative rather than diffing it.
  3. Nothing needs deleting on your side. Old IDs keep working: GET /api/v1/contacts/{merged_away_id} still returns the surviving contact, sets an X-Merged-Into response header with the surviving ID, and includes a merged_from object naming the ID you asked for. Requests that address a merged-away contact are served, not rejected. Treat the header as a safety net for references you already stored, not as your merge signal — the event tells you sooner.

The survivor inherits the history, not just the identifiers. Everything the absorbed record carried moves across, so a merge never costs you history:

  • conversations and messages, tags, notes and custom fields;
  • list subscriptions together with the consent record behind them (when and how the person opted in);
  • campaign send history and email engagement, pending scheduled sends, and any unsubscribe link already mailed out — old links keep working and still unsubscribe the right person.

Email engagement for the same campaign is combined into a single per-recipient record: opens and clicks are added together, and the first/last timestamps widen to cover both. Campaign reports therefore show one line per person after a merge rather than two.

Only the survivor gets an event

There is no separate event for the contacts that were absorbed — no contact.deleted is sent for them. A merge is not a deletion: the person still exists under data.contact.id, and firing a delete would tell integrations to remove a customer who is still active. merged_from is the signal to act on.

contact.updated may fire more than once for one logical change

A merge that is part of a larger operation (a live-chat visitor identifying themselves, a WhatsApp phone-number reveal) can be followed by ordinary contact.updated events for the same contact as the rest of the operation completes. Make your handler idempotent and treat the newest payload as truth rather than assuming one event per change.


Campaign Events

link.clicked

Triggered when a tracked link in a campaign message is clicked.

{
"id": "evt_link_001",
"type": "link.clicked",
"event_id": "evt_link_001",
"created_at": "2026-03-04T16:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"url": "https://shop.example.com/spring-sale",
"contact_id": "ct_456",
"campaign_id": "camp_a1b2c3d4",
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": [
{
"id": "cm_790",
"method_type": "phone",
"value": "+1234567890",
"channel_id": null,
"display_name": null,
"phone_type": "mobile",
"is_primary": true,
"email_status": null,
"last_used_at": null,
"created_at": "2026-03-04T10:29:00Z"
}
]
}
}
}

Email Events

Email events have a different payload structure than message events, including email-specific fields like subject, body HTML, and threading information.

email.received

Triggered when an inbound email is received.

{
"id": "evt_email_001",
"type": "email.received",
"event_id": "evt_email_001",
"created_at": "2026-03-04T16:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_789",
"conversation_id": "conv_email_123",
"contact_id": "ct_sender",
"platform": "email",
"subject": "Question about my invoice",
"body_html": "<p>Hi, I have a question about my January invoice...</p>",
"from_id": "[email protected]",
"to_id": "[email protected]",
"cc": [],
"email_integration_id": "ei_abc123",
"email_thread_id": "thread_def456",
"created_at": "2026-03-04T16:00:00Z"
}
}

email.sent

Triggered when an outbound email is sent.

{
"id": "evt_email_002",
"type": "email.sent",
"event_id": "evt_email_002",
"created_at": "2026-03-04T16:05:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_790",
"conversation_id": "conv_email_123",
"contact_id": "ct_sender",
"platform": "email",
"subject": "Re: Question about my invoice",
"from_id": "[email protected]",
"to_id": "[email protected]",
"cc": [],
"email_integration_id": "ei_abc123",
"email_thread_id": "thread_def456",
"sent_at": "2026-03-04T16:05:00Z"
}
}

email.delivered

Triggered when email delivery is confirmed.

{
"id": "evt_email_003",
"type": "email.delivered",
"event_id": "evt_email_003",
"created_at": "2026-03-04T16:05:10Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_790",
"conversation_id": "conv_email_123",
"contact_id": "ct_sender",
"platform": "email",
"recipient": "[email protected]",
"delivered_at": "2026-03-04T16:05:10Z"
}
}

email.bounced

Triggered when an email bounces (hard or soft).

{
"id": "evt_email_004",
"type": "email.bounced",
"event_id": "evt_email_004",
"created_at": "2026-03-04T16:05:15Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_791",
"conversation_id": "conv_email_124",
"contact_id": "ct_bounced",
"platform": "email",
"bounce_type": "hard",
"recipient": "[email protected]",
"error_message": "550 5.1.1 The email account that you tried to reach does not exist"
}
}
FieldDescription
bounce_typehard or soft
recipientEmail address that bounced
error_messageBounce reason from the mail server

email.opened

Triggered when a recipient opens an email (via tracking pixel).

{
"id": "evt_email_005",
"type": "email.opened",
"event_id": "evt_email_005",
"created_at": "2026-03-04T17:20:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_790",
"conversation_id": "conv_email_123",
"contact_id": "ct_sender",
"platform": "email",
"recipient": "[email protected]",
"opened_at": "2026-03-04T17:20:00Z"
}
}

email.complained

Triggered when a recipient marks the email as spam.

{
"id": "evt_email_006",
"type": "email.complained",
"event_id": "evt_email_006",
"created_at": "2026-03-04T18:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"message_id": "msg_email_790",
"conversation_id": "conv_email_123",
"contact_id": "ct_sender",
"platform": "email",
"recipient": "[email protected]",
"complained_at": "2026-03-04T18:00:00Z"
}
}

Channel Events

Channel events fire over the lifecycle of a channel — a connected messaging integration such as a WhatsApp number, a Telegram bot, an SMS sender, a Messenger/Instagram page, or an email mailbox. They let you react when an integration is connected, changes state (including provider-driven disconnects), or is removed — without polling GET /api/v1/channels.

These events are especially useful if you onboard channels for your customers via channel connect links: subscribe to channel.created to learn the moment a customer finishes connecting, and to channel.updated to be alerted when one of their channels goes unhealthy so you can prompt them to reconnect.

All three events carry the same channel object in data. The channel object never contains credentials, tokens, or the channel's decrypted configuration — only public/operational fields.

The channel object

FieldTypeDescription
idstringChannel ID (stable; matches channel_id on message events).
tenant_idstringThe tenant that owns the channel.
platformstringChannel platform: whatsapp, telegram, messenger, instagram, sms, viber, live_chat, browser_push, email.
namestring | nullUser-assigned display name.
identifierstring | nullThe channel's external identifier — phone number, @botname, page ID, or email address, depending on platform.
is_activebooleanWhether the channel is currently active (able to send/receive). Flips to false on disconnect.
is_verifiedbooleanWhether the channel has completed verification.
is_archivedbooleanWhether the channel has been archived (soft-deleted).
statusstringConvenience roll-up: connected when active and not archived, otherwise disconnected.
disconnection_reasonstring | nullWhy the channel was disconnected (e.g. token_invalidated, partner_removed, account_offboarded). null when connected.
disconnected_atstring | nullISO 8601 timestamp the channel was last disconnected.
reconnected_atstring | nullISO 8601 timestamp the channel was last reconnected.
created_via_connect_token_idstring | nullThe channel connect token used to create this channel, if it was onboarded via a connect link. null for channels created directly in the app.
created_atstringISO 8601 creation timestamp.
updated_atstring | nullISO 8601 last-update timestamp.

channel.created

Triggered when a channel is connected — through the app, the API, an OAuth flow, or a channel connect link.

{
"id": "evt_channel_001",
"type": "channel.created",
"event_id": "ch_123",
"created_at": "2026-03-04T09:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"channel": {
"id": "ch_123",
"tenant_id": "tenant_abc123",
"platform": "whatsapp",
"name": "Support WhatsApp",
"identifier": "+1987654321",
"is_active": true,
"is_verified": true,
"is_archived": false,
"status": "connected",
"disconnection_reason": null,
"disconnected_at": null,
"reconnected_at": null,
"created_via_connect_token_id": "cct_9a8b7c6d",
"created_at": "2026-03-04T09:00:00Z",
"updated_at": null
}
}
}

channel.updated

Triggered when a channel changes without being created or removed. The top-level data.change field classifies the transition:

changeMeaning
status_changedConnection state changed — the channel was disconnected (e.g. token revoked, partner removed) or reconnected. Inspect is_active, status, disconnection_reason, disconnected_at, and reconnected_at. This is the event to watch for channel-health alerting.
settings_updatedA user edited the channel's name, identifier, verification state, or configuration.
restoredAn archived channel was un-archived (restored).

Example — a channel goes offline (this is how you find out an integration needs attention):

{
"id": "evt_channel_002",
"type": "channel.updated",
"event_id": "ch_123",
"created_at": "2026-03-04T14:22:00Z",
"tenant_id": "tenant_abc123",
"data": {
"change": "status_changed",
"channel": {
"id": "ch_123",
"tenant_id": "tenant_abc123",
"platform": "whatsapp",
"name": "Support WhatsApp",
"identifier": "+1987654321",
"is_active": false,
"is_verified": true,
"is_archived": false,
"status": "disconnected",
"disconnection_reason": "token_invalidated",
"disconnected_at": "2026-03-04T14:22:00Z",
"reconnected_at": null,
"created_via_connect_token_id": "cct_9a8b7c6d",
"created_at": "2026-03-04T09:00:00Z",
"updated_at": "2026-03-04T14:22:00Z"
}
}
}
Build channel-health alerts on status_changed

When data.change == "status_changed" and channel.status == "disconnected", surface disconnection_reason to the affected customer and point them at a reconnect flow. When the same channel later comes back, you receive another channel.updated with status: "connected" and a populated reconnected_at.

channel.deleted

Triggered when a channel is removed — either archived (the normal in-app "delete", which is a soft delete) or permanently deleted. The payload is a snapshot of the channel as it was at removal time.

{
"id": "evt_channel_003",
"type": "channel.deleted",
"event_id": "ch_123",
"created_at": "2026-03-05T08:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"channel": {
"id": "ch_123",
"tenant_id": "tenant_abc123",
"platform": "whatsapp",
"name": "Support WhatsApp",
"identifier": "+1987654321",
"is_active": false,
"is_verified": true,
"is_archived": true,
"status": "disconnected",
"disconnection_reason": null,
"disconnected_at": null,
"reconnected_at": null,
"created_via_connect_token_id": "cct_9a8b7c6d",
"created_at": "2026-03-04T09:00:00Z",
"updated_at": "2026-03-05T08:00:00Z"
}
}
}
Scope channel events to specific channels

Like message events, channel events respect a webhook endpoint's channel source filter. If you configure an endpoint to only receive events for selected channels, channel.* events are matched on data.channel.id.


Comment Events

Comment events fire when someone comments on one of your social posts — an Instagram post or reel, or a Facebook Page post. They are the way to react to public comments without polling, and they fire for every comment, whether or not you have an automation set up to answer it.

Comments are not conversations: a comment has no conversation and no message, so data carries the post and the comment directly.

comment.received

Triggered when a new comment is posted on one of your Instagram or Facebook posts. Only new comments trigger it — edits, hides and deletions have their own events (comment.updated, comment.deleted, below).

{
"id": "evt_comment_001",
"type": "comment.received",
"event_id": "17912345678901234",
"created_at": "2026-03-04T16:00:05Z",
"tenant_id": "tenant_abc123",
"data": {
"platform": "instagram",
"channel_id": "ch_123",
"post": {
"id": "sp_a1b2c3d4",
"external_id": "17998877665544332",
"permalink": "https://www.instagram.com/p/CxAmPlE1234/"
},
"comment": {
"id": "sc_e5f6a7b8",
"external_id": "17912345678901234",
"text": "Is this still available in blue?",
"created_at": "2026-03-04T16:00:00Z",
"permalink": "https://www.instagram.com/p/CxAmPlE1234/c/17912345678901234/",
"parent_external_comment_id": null,
"is_reply": false,
"private_reply_window_expires_at": "2026-03-11T16:00:00Z",
"author": {
"external_id": "78901234567890123",
"username": "jane.doe",
"name": "jane.doe"
}
},
"auto_replied": false
}
}

Comment Fields Reference

FieldTypeDescription
platformstringWhere the comment was posted: instagram or messenger (Facebook Page).
channel_idstringThe channel the post belongs to. Matches channel_id on message events.
post.idstring | nullSendSeven's ID for the post.
post.external_idstringThe post's ID on Instagram or Facebook.
post.permalinkstring | nullPublic link to the post. May be null the first time a post is seen.
comment.idstring | nullSendSeven's ID for the comment.
comment.external_idstringThe comment's ID on Instagram or Facebook. Stable — use it to de-duplicate.
comment.textstringThe comment text.
comment.created_atstring | nullISO 8601 timestamp of when the comment was posted.
comment.permalinkstring | nullPublic link to the comment, when the platform provides one.
comment.parent_external_comment_idstring | nullThe comment this one replies to. null for a top-level comment.
comment.is_replybooleantrue when the comment is a reply to another comment.
comment.private_reply_window_expires_atstring | nullISO 8601 deadline for sending a private reply to this comment. See below.
comment.author.external_idstring | nullThe commenter's ID on the platform.
comment.author.usernamestring | nullThe commenter's Instagram handle. Always null on Facebook, which does not expose handles.
comment.author.namestring | nullThe commenter's display name (Instagram: the handle).
auto_repliedbooleantrue when one of your automations already responded to this comment.
Private replies are one-shot and time-limited

Instagram and Facebook allow exactly one private reply (a direct message) per comment, and only within 7 days of the comment being posted. private_reply_window_expires_at is included so you can tell at a glance whether a private reply is still possible without calling the API. On Facebook, private replies are not available for comments on ads.

See Comments to Private Replies for the full picture: reacting to this event, sending a private reply, and answering comments automatically.

Check auto_replied before responding

If you run your own tooling alongside SendSeven automations, use auto_replied to avoid answering a comment that has already been answered.

Scope comment events to specific channels

Like message events, comment events respect a webhook endpoint's channel source filter. If you configure an endpoint to only receive events for selected channels, comment.received, comment.updated, comment.deleted and every post.* event are matched on data.channel_id.

comment.updated

Triggered when an existing comment on one of your posts is edited or hidden/unhidden. The change field says which. comment.text and comment.is_hidden carry the current values.

{
"id": "evt_comment_010",
"type": "comment.updated",
"event_id": "17912345678901234",
"created_at": "2026-03-04T16:05:00Z",
"tenant_id": "tenant_abc123",
"data": {
"platform": "messenger",
"channel_id": "ch_123",
"post": {
"id": "sp_a1b2c3d4",
"external_id": "301234567890_401234567890",
"permalink": "https://www.facebook.com/301234567890/posts/401234567890"
},
"comment": {
"id": "sc_e5f6a7b8",
"external_id": "401234567890_501234567890",
"text": "Is this still available in navy?",
"created_at": "2026-03-04T16:00:00Z",
"permalink": null,
"parent_external_comment_id": null,
"is_reply": false,
"is_hidden": false,
"deleted_at": null,
"author": {
"external_id": "78901234567890123",
"username": null,
"name": "Jane Doe"
}
},
"change": "edited"
}
}
  • change is one of edited (text changed), hidden (the Page hid the comment) or unhidden (the Page un-hid it).
  • Facebook Pages only. Instagram's comment webhook is create-only, so comment.updated effectively never fires on Instagram.

comment.deleted

Triggered when a comment is removed. The comment is kept on our side as a soft-deleted record (so it can still show as "deleted" in history); comment.deleted_at is the time we recorded the removal.

{
"id": "evt_comment_011",
"type": "comment.deleted",
"event_id": "401234567890_501234567890",
"created_at": "2026-03-04T16:10:00Z",
"tenant_id": "tenant_abc123",
"data": {
"platform": "messenger",
"channel_id": "ch_123",
"post": {
"id": "sp_a1b2c3d4",
"external_id": "301234567890_401234567890",
"permalink": "https://www.facebook.com/301234567890/posts/401234567890"
},
"comment": {
"id": "sc_e5f6a7b8",
"external_id": "401234567890_501234567890",
"text": "Is this still available in navy?",
"created_at": "2026-03-04T16:00:00Z",
"is_hidden": false,
"deleted_at": "2026-03-04T16:10:00Z",
"author": { "external_id": "78901234567890123", "username": null, "name": "Jane Doe" }
}
}
}
Facebook is real-time; Instagram is at-sync-only

Facebook Pages push edits, hides and deletions in real time, so comment.updated / comment.deleted arrive within seconds. Instagram has no delete/edit webhook — a deleted Instagram comment is only discovered the next time the post is synced (for example when you call posts-sync or open the post in SendSeven), so comment.deleted on Instagram can lag, and comment.updated is not emitted for Instagram at all.

Post Events

Post events track the social posts themselves — the Instagram/Facebook posts your comments hang off. A post enters SendSeven either when a comment arrives on it or when the posts list is synced.

post.created

Fired when a post is pulled into SendSeven for the first time (a posts-sync or the on-connect backfill inserts a post we had never seen). This is a materialisation event — the post may have existed on Meta long before we synced it. event_id is the post's Meta ID, so re-syncing the same post never re-fires it.

{
"id": "evt_post_001",
"type": "post.created",
"event_id": "301234567890_401234567890",
"created_at": "2026-03-04T15:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"platform": "instagram",
"channel_id": "ch_123",
"post": {
"id": "sp_a1b2c3d4",
"external_id": "17998877665544332",
"permalink": "https://www.instagram.com/p/CxAmPlE1234/"
}
}
}

post.updated

Fired when a synced post's fields actually change — a re-sync that refreshes its caption, media or engagement counts, or a Facebook edited push for the post. Not fired when nothing changed.

post.deleted

Fired when a post is removed. Facebook pushes this in real time; the post is soft-deleted on our side and drops out of the posts list. As with comments, Instagram has no post-delete webhook, so an Instagram post is not marked deleted this way.

{
"id": "evt_post_003",
"type": "post.deleted",
"event_id": "301234567890_401234567890",
"created_at": "2026-03-04T17:00:00Z",
"tenant_id": "tenant_abc123",
"data": {
"platform": "messenger",
"channel_id": "ch_123",
"post": {
"id": "sp_a1b2c3d4",
"external_id": "301234567890_401234567890",
"permalink": "https://www.facebook.com/301234567890/posts/401234567890"
}
}
}

All post.* events carry the same post object (id, external_id, permalink) plus top-level platform and channel_id.


Team Chat Events

team_chat.message.created

Triggered when a message is posted to a Team Chat channel that has "Allow Bots to send messages" enabled -- fired for both human and bot senders, since the gate is on the channel, not the sender. Never fired for the Knowledge Base system channel, and never fired for direct messages -- see Team Chat Webhook for the full gating rules, the complete payload reference, and the DM caveat in detail.

{
"id": "evt_7f3a2b1c9d8e4f56",
"type": "team_chat.message.created",
"event_id": "9b21e7c4-13ad-4f0a-8f52-6c9d0e3a7711",
"created_at": "2026-08-04T14:30:00Z",
"tenant_id": "tenant_abc123",
"data": {
"channel": {
"id": "3f7c1a92-5d84-4b1e-9a3c-70e2f8d41b05",
"name": "general",
"display_name": "General",
"allow_bots": true,
"is_system": false
},
"message": {
"id": "9b21e7c4-13ad-4f0a-8f52-6c9d0e3a7711",
"channel_id": "3f7c1a92-5d84-4b1e-9a3c-70e2f8d41b05",
"text": "Deploy finished successfully :rocket:",
"sender_type": "bot",
"sender_user_id": null,
"sender_username": "Deploy Bot",
"bot_name": "Deploy Bot",
"mentioned_user_ids": [],
"mentioned_all": false,
"parent_message_id": null,
"attachment_ids": [],
"attachments": [],
"created_at": "2026-08-04T14:30:00Z"
}
}
}
Direct messages never trigger this event

Only channel messages emit team_chat.message.created -- direct messages (including bot DMs) are invisible to webhooks entirely, with no opt-in available. See Team Chat Webhook.


Event Summary Table

EventTrigger
message.receivedNew inbound message from a contact
message.sentOutbound message sent to channel provider
message.deliveredDelivery confirmed by channel
message.readRead receipt received from channel
message.reactionEmoji reaction added to or removed from a message (data.reaction.action = added/removed). Free — never billed.
message.failedMessage delivery failed
conversation.createdNew conversation started
conversation.closedConversation closed/resolved
conversation.assignedConversation assigned to a user
conversation.reopenedClosed conversation reopened
conversation.updatedConversation snoozed or unsnoozed
conversation.transcript.createdTranscript export (PDF/ZIP) finished generating and is ready to download
contact.createdNew contact added
contact.updatedContact information changed, or two contacts were merged into this one
contact.deletedContact deleted
contact.subscribedContact subscribed to a list
contact.unsubscribedContact unsubscribed from a list
campaign.message.sentCampaign message sent to a contact
campaign.message.deliveredCampaign message delivery confirmed
campaign.message.readCampaign message read by the recipient
campaign.message.failedCampaign message delivery failed
campaign.email.sentCampaign email sent to a contact
campaign.email.deliveredCampaign email delivery confirmed
campaign.email.bouncedCampaign email bounced
campaign.email.openedCampaign email opened
campaign.email.complainedCampaign email marked as spam
channel.createdA channel (messaging integration) was connected
channel.updatedA channel changed status (disconnected/reconnected), had its settings edited, or was restored
channel.deletedA channel was removed (archived or permanently deleted)
comment.receivedSomeone commented on one of your Instagram or Facebook posts
comment.updatedAn existing comment was edited or hidden/unhidden (Facebook real-time; not on Instagram)
comment.deletedA comment was removed (Facebook real-time; Instagram only at post re-sync)
post.createdA social post was pulled into SendSeven for the first time
post.updatedA synced post's caption, media or counts changed
post.deletedA social post was removed (Facebook real-time only)
team_chat.message.createdA message was posted to a bot-enabled Team Chat channel (human or bot sender; never fires for DMs)
link.clickedTracked link clicked
email.receivedInbound email received
email.sentOutbound email sent
email.deliveredEmail delivery confirmed
email.bouncedEmail bounced
email.openedEmail opened
email.complainedEmail marked as spam
Subscribe to exactly what you need

This table is the complete, authoritative list of events you can subscribe to (mirrors the API's validation list). Detailed data payloads are shown above for the most common events; conversation.assigned, conversation.reopened, contact.deleted, contact.subscribed, contact.unsubscribed, and the campaign.* families all use the same envelope with event-appropriate data. If you need a field-level payload example for one of these that is not shown above, contact dev support so we can document the exact shape rather than have you guess.

Next Steps