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
}
}
| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (for deduplication) |
type | string | Event type (e.g., message.received) |
event_id | string | Alias for id, included for convenience |
created_at | string | ISO 8601 timestamp |
tenant_id | string | Your tenant ID |
data | object | Event-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.)
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:
| Field | Type | Description |
|---|---|---|
emoji | string | The reaction emoji. For a removal the platform may send an empty string. |
action | string | "added" or "removed". |
from_id | string | null | Platform 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
| Field | Type | Description |
|---|---|---|
id | string | Unique message ID |
conversation_id | string | Parent conversation ID |
platform | string | Channel platform (whatsapp, telegram, sms, messenger, instagram, email) |
channel_id | string | Channel configuration ID |
contact_id | string | Contact ID |
contact_method_id | string | null | FK 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). |
direction | string | inbound or outbound |
message_type | string | text, image, video, audio, document, sticker, location, template, button, interactive |
text | string | null | Message text content |
status | string | received, sent, delivered, read, failed |
from_id | string | Sender identifier (phone number, platform ID) |
to_id | string | null | Recipient identifier |
external_id | string | null | Provider message ID (e.g., WhatsApp wamid) |
reply_to_external_id | string | null | Inbound. 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_id | string | null | Inbound. 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. |
meta | object | Additional 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_info | object | null | Inbound 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_at | string | ISO 8601 creation timestamp |
sent_at | string | null | ISO 8601 sent timestamp |
delivered_at | string | null | ISO 8601 delivery confirmation timestamp |
attachments | array | null | File attachments with download URLs (see Attachments below) |
read_at | string | null | ISO 8601 read receipt timestamp |
contact_infoInbound 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):
| Field | Type | Description |
|---|---|---|
id | string | Unique attachment ID |
type | string | Attachment type: image, video, audio, document, sticker, file |
filename | string | Original or generated filename |
content_type | string | MIME type (e.g., image/jpeg, application/pdf) |
file_size | integer | null | File size in bytes |
url | string | Stable API download URL — requires authentication, never expires |
signed_url | string | Pre-signed GCS URL — no authentication required, expires in 24 hours |
url for reliable accessThe 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 theexternal_idthe provider gave that original message.reply_to_id— the SendSeven messageidthatreply_to_external_idresolved to, so you can link straight to the original message in your own copy of the conversation. Best-effort: it isnullwhen 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_idis 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:
| Channel | Source |
|---|---|
context.id (quoted reply and quick-reply / interactive button taps) | |
| Facebook Messenger | reply_to.mid |
reply_to.mid | |
| Telegram | reply_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
| Field | Type | Description |
|---|---|---|
id | string | Conversation ID |
channel_id | string | Channel configuration ID |
contact_id | string | null | Contact ID |
assigned_user_id | string | null | User the conversation is assigned to, if any. |
bot_session_id | string | null | Active bot/automation session ID, if the conversation is being handled by a bot. |
status | string | open, closed, pending |
subject | string | null | Conversation subject |
last_customer_message_at | string | null | ISO 8601 timestamp of the most recent inbound customer message. |
last_agent_reply_at | string | null | ISO 8601 timestamp of the most recent agent reply. |
snoozed_until | string | null | ISO 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_message | boolean | null | Whether an incoming customer message auto-reopens the snoozed conversation. null when not snoozed. |
created_at | string | ISO 8601 creation timestamp |
Contact Fields Reference
| Field | Type | Description |
|---|---|---|
id | string | Contact ID |
name | string | null | Contact display name |
email | string | null | Contact email address (denormalized from primary email method) |
phone | string | null | Contact phone number (denormalized from primary phone method) |
contact_methods | array | All 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.
| Field | Type | Description |
|---|---|---|
id | string | Contact method ID (matches message.contact_method_id) |
method_type | string | phone, email, whatsapp_id, whatsapp_bsuid, telegram_id, messenger_id, instagram_id, or a social-profile type (linkedin, homepage, facebook, instagram_handle, social_other) |
value | string | The identifier value (phone number, email, platform ID) |
channel_id | string | null | Channel ID for channel-scoped methods (whatsapp_bsuid, messenger_id, instagram_id); null for global methods |
display_name | string | null | Display name associated with the method. For whatsapp_bsuid, the contact's public WhatsApp username when known (a label -- it is not addressable) |
phone_type | string | null | For phone methods: mobile, landline, etc. |
is_primary | boolean | Whether this is the primary method of its type |
email_status | string | null | For email methods: validation/deliverability status |
last_used_at | string | null | ISO 8601 timestamp the method was last used |
created_at | string | ISO 8601 creation timestamp |
method_type as an open enumNew 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 field | Description |
|---|---|
change | The transition: "snoozed" or "unsnoozed". |
conversation | The updated conversation object (includes snoozed_until and snooze_reopen_on_message). |
actor | The user who triggered the change. Omitted for system-triggered changes such as auto-reopen when a customer replies to a snoozed conversation. |
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 field | Type | Description |
|---|---|---|
conversation_id | string | Conversation the transcript was exported from. |
tenant_id | string | Your tenant ID (also on the envelope). |
contact_id | string | null | Contact on the conversation, if the conversation has one. |
format | string | Export format: pdf or zip. |
job_id | string | The export job ID. Also used as the event event_id. |
download_url | string | Pre-signed, short-lived download URL for the artifact (forces download with a friendly filename). |
download_url_expires_at | string | ISO 8601 UTC timestamp when download_url expires. |
download_url_expires_in_minutes | integer | Lifetime of download_url in minutes. |
poll_url | string | Durable API path to re-fetch a fresh download URL after the signed URL expires — GET /api/v1/conversations/{conversation_id}/transcript/{job_id}. |
filename | string | Suggested download filename. |
download_url is short-livedThe 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"
}
}
}
| Field | Type | Description |
|---|---|---|
merged_from | array of string | The contact IDs that were absorbed. These IDs no longer identify a separate contact. Usually one entry; a single merge can absorb several. |
merged_at | string (ISO 8601) | When the merge happened. |
reason | string | What 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:
- Update your stored mapping so the absorbed IDs now resolve to
data.contact.id. - Re-read
data.contact.contact_methodsin 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 isis_primary— so treat the array as authoritative rather than diffing it. - Nothing needs deleting on your side. Old IDs keep working:
GET /api/v1/contacts/{merged_away_id}still returns the surviving contact, sets anX-Merged-Intoresponse header with the surviving ID, and includes amerged_fromobject 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.
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 changeA 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"
}
}
| Field | Description |
|---|---|
bounce_type | hard or soft |
recipient | Email address that bounced |
error_message | Bounce 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
| Field | Type | Description |
|---|---|---|
id | string | Channel ID (stable; matches channel_id on message events). |
tenant_id | string | The tenant that owns the channel. |
platform | string | Channel platform: whatsapp, telegram, messenger, instagram, sms, viber, live_chat, browser_push, email. |
name | string | null | User-assigned display name. |
identifier | string | null | The channel's external identifier — phone number, @botname, page ID, or email address, depending on platform. |
is_active | boolean | Whether the channel is currently active (able to send/receive). Flips to false on disconnect. |
is_verified | boolean | Whether the channel has completed verification. |
is_archived | boolean | Whether the channel has been archived (soft-deleted). |
status | string | Convenience roll-up: connected when active and not archived, otherwise disconnected. |
disconnection_reason | string | null | Why the channel was disconnected (e.g. token_invalidated, partner_removed, account_offboarded). null when connected. |
disconnected_at | string | null | ISO 8601 timestamp the channel was last disconnected. |
reconnected_at | string | null | ISO 8601 timestamp the channel was last reconnected. |
created_via_connect_token_id | string | null | The 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_at | string | ISO 8601 creation timestamp. |
updated_at | string | null | ISO 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:
change | Meaning |
|---|---|
status_changed | Connection 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_updated | A user edited the channel's name, identifier, verification state, or configuration. |
restored | An 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"
}
}
}
status_changedWhen 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"
}
}
}
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
| Field | Type | Description |
|---|---|---|
platform | string | Where the comment was posted: instagram or messenger (Facebook Page). |
channel_id | string | The channel the post belongs to. Matches channel_id on message events. |
post.id | string | null | SendSeven's ID for the post. |
post.external_id | string | The post's ID on Instagram or Facebook. |
post.permalink | string | null | Public link to the post. May be null the first time a post is seen. |
comment.id | string | null | SendSeven's ID for the comment. |
comment.external_id | string | The comment's ID on Instagram or Facebook. Stable — use it to de-duplicate. |
comment.text | string | The comment text. |
comment.created_at | string | null | ISO 8601 timestamp of when the comment was posted. |
comment.permalink | string | null | Public link to the comment, when the platform provides one. |
comment.parent_external_comment_id | string | null | The comment this one replies to. null for a top-level comment. |
comment.is_reply | boolean | true when the comment is a reply to another comment. |
comment.private_reply_window_expires_at | string | null | ISO 8601 deadline for sending a private reply to this comment. See below. |
comment.author.external_id | string | null | The commenter's ID on the platform. |
comment.author.username | string | null | The commenter's Instagram handle. Always null on Facebook, which does not expose handles. |
comment.author.name | string | null | The commenter's display name (Instagram: the handle). |
auto_replied | boolean | true when one of your automations already responded to this comment. |
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.
auto_replied before respondingIf you run your own tooling alongside SendSeven automations, use auto_replied to avoid answering a comment that has already been answered.
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"
}
}
changeis one ofedited(text changed),hidden(the Page hid the comment) orunhidden(the Page un-hid it).- Facebook Pages only. Instagram's comment webhook is create-only, so
comment.updatedeffectively 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 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"
}
}
}
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
| Event | Trigger |
|---|---|
message.received | New inbound message from a contact |
message.sent | Outbound message sent to channel provider |
message.delivered | Delivery confirmed by channel |
message.read | Read receipt received from channel |
message.reaction | Emoji reaction added to or removed from a message (data.reaction.action = added/removed). Free — never billed. |
message.failed | Message delivery failed |
conversation.created | New conversation started |
conversation.closed | Conversation closed/resolved |
conversation.assigned | Conversation assigned to a user |
conversation.reopened | Closed conversation reopened |
conversation.updated | Conversation snoozed or unsnoozed |
conversation.transcript.created | Transcript export (PDF/ZIP) finished generating and is ready to download |
contact.created | New contact added |
contact.updated | Contact information changed, or two contacts were merged into this one |
contact.deleted | Contact deleted |
contact.subscribed | Contact subscribed to a list |
contact.unsubscribed | Contact unsubscribed from a list |
campaign.message.sent | Campaign message sent to a contact |
campaign.message.delivered | Campaign message delivery confirmed |
campaign.message.read | Campaign message read by the recipient |
campaign.message.failed | Campaign message delivery failed |
campaign.email.sent | Campaign email sent to a contact |
campaign.email.delivered | Campaign email delivery confirmed |
campaign.email.bounced | Campaign email bounced |
campaign.email.opened | Campaign email opened |
campaign.email.complained | Campaign email marked as spam |
channel.created | A channel (messaging integration) was connected |
channel.updated | A channel changed status (disconnected/reconnected), had its settings edited, or was restored |
channel.deleted | A channel was removed (archived or permanently deleted) |
comment.received | Someone commented on one of your Instagram or Facebook posts |
comment.updated | An existing comment was edited or hidden/unhidden (Facebook real-time; not on Instagram) |
comment.deleted | A comment was removed (Facebook real-time; Instagram only at post re-sync) |
post.created | A social post was pulled into SendSeven for the first time |
post.updated | A synced post's caption, media or counts changed |
post.deleted | A social post was removed (Facebook real-time only) |
team_chat.message.created | A message was posted to a bot-enabled Team Chat channel (human or bot sender; never fires for DMs) |
link.clicked | Tracked link clicked |
email.received | Inbound email received |
email.sent | Outbound email sent |
email.delivered | Email delivery confirmed |
email.bounced | Email bounced |
email.opened | Email opened |
email.complained | Email marked as spam |
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
- Message Types Reference -- detailed payloads for every message type
- Webhook Setup -- create and manage webhooks
- Signature Verification -- secure your webhook endpoint