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_idstringIdentifier of the underlying occurrence, the same on every retry of that delivery. It can differ from id (for message events it is typically the message ID; for conversation.assigned it is unique per assignment).
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"
}
}
}
Newsletter keywords (START, STOP, STOP ALL, list keywords)

A keyword message is always stored, but whether message.received fires depends on the conversation:

  • The contact has an open conversation on that channel: the keyword arrives in it as a normal message and message.received fires. The confirmation or goodbye reply is stored right after it.
  • No open conversation on that channel: no conversation is created and no message.received event fires. The keyword is stored with conversation_id: null and the channel set, and is moved into the contact's next conversation on that channel (without notifications or unread counts). The confirmation or goodbye reply is sent but not stored. Use the subscription itself (the contact's list subscriptions) as the source of truth for opt-ins and opt-outs.

A keyword never lands in a conversation on another channel. In automations it opens the WhatsApp 24-hour window on that number, so a Send WhatsApp Flow or Send message step can follow right after a START.

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.

Reactions are not messages. An inbound reaction never fires message.received, never starts or resumes a flow, never triggers a bot or agent reply, and never reopens a conversation. message.reaction fires when the reacted-to message is stored in SendSeven. When it is not (for example a message sent before the channel was connected), no webhook fires; if the contact has a conversation on that channel, the inbox shows a system note ("Reacted 👍 to a message") whose meta.is_reaction is true and whose meta.reaction holds emoji, action, target_external_id and from_id.

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": {
"last_failed_error": {
"captured_at": "2026-03-04T10:35:10+00:00",
"errors": [
{
"code": 131047,
"title": "Re-engagement message",
"message": "Re-engagement message",
"error_data": {
"details": "Message failed to send because more than 24 hours have passed since the customer last replied to this number."
}
}
]
}
},
"created_at": "2026-03-04T10:35:08Z",
"sent_at": "2026-03-04T10:35:09Z",
"delivered_at": null,
"read_at": null
},
"error": "Re-engagement message: Re-engagement message | details: Message failed to send because more than 24 hours have passed since the customer last replied to this number.",
"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.failed fires in two situations:

  • The channel reports the failure after it accepted the message (for example a WhatsApp delivery receipt with status failed).
  • SendSeven records the failure while sending, with POST /api/v1/messages or the rich send endpoints (POST /messages/send/interactive, /send/contacts, /send/sticker, /send/carousel). Examples: an SMS blocked because the workspace's prepaid SMS balance is at €0 (insufficient_sms_balance), a disconnected channel, a blocked or missing contact, a provider that rejects the send, or SendSeven giving up after several attempts. A send that is retried fires nothing until the last attempt has failed, and the event fires only once per message.

Where to find the reason:

FieldWhen it is setContent
data.errorAlwaysThe failure reason as text. For WhatsApp receipts it joins each Meta error as title: message, plus details, subcode and href when Meta sends them.
data.message.meta.last_failed_errorBoth situationscaptured_at plus errors[]. For a WhatsApp receipt these are Meta's original error objects (numeric code, title, message, error_data.details). For a failure recorded while sending there is one entry with code and message, where code can be a SendSeven code such as insufficient_sms_balance rather than a provider number. It stays in meta if the message is sent again later.
data.message.error_code and data.message.failed_atFailures recorded while sendingThe same code as above (for example insufficient_sms_balance, send_given_up or rate_limit_deferral_expired) and the failure time.
data.message.meta.error_reasonWhen a stable reason is knownA machine-readable reason next to error_code, for example thread_controlled_by_other_app when another WhatsApp provider controls the conversation (error 131070). See Conversation routing.

The message also carries error_code and error_message on GET /api/v1/messages/{message_id}, whichever way it failed. Read them there when you need one place for every case.

Failures that do not fire message.failed:

  • A send request that is rejected before a message is created (a 4xx such as WHATSAPP_WINDOW_EXPIRED or a validation error) returns the error in its HTTP response only. No message exists, so nothing fires.
  • Email sends that fail in the email pipeline.
  • Campaign sends: they fire campaign.message.failed instead.

attachment.summarized​

Triggered when an AI summary of an image or PDF a contact sent is ready. It fires once each time a summary is completed, in two cases:

  • a Conversational Agent read the file while answering (summary.source = "agent"), or
  • a team member (or an API client) asked for a summary with Summarize (summary.source = "on_demand").

It fires only for successful summaries and only while AI features are enabled for your workspace. It is a separate event from message.received: the message webhook is sent as soon as the message arrives, and the summary follows later. Link the two with data.message_id and data.attachment_id.

{
"id": "evt_8a1f3c5e7b9d0f12",
"type": "attachment.summarized",
"created_at": "2026-10-06T10:00:06Z",
"tenant_id": "990e8400-e29b-41d4-a716-446655440000",
"event_id": "3c9d2b7e-6a41-5f08-9e2d-7b1c4a5d6e8f",
"data": {
"attachment_id": "aa0e8400-e29b-41d4-a716-446655440000",
"message_id": "660e8400-e29b-41d4-a716-446655440000",
"conversation_id": "770e8400-e29b-41d4-a716-446655440000",
"contact_id": "880e8400-e29b-41d4-a716-446655440000",
"channel_id": "550e8400-e29b-41d4-a716-446655440000",
"summary": {
"status": "ok",
"kind": "pdf",
"text": "SUMMARY: Invoice no. 2026-118 from Example GmbH.\nKEY FACTS: Total 120.00 EUR; due 2026-10-20",
"sections": [
{"key": "summary", "text": "Invoice no. 2026-118 from Example GmbH."},
{"key": "key_facts", "text": "Total 120.00 EUR; due 2026-10-20"}
],
"pages_read": 10,
"pages_total": 34,
"truncated": true,
"model": "gemini-3.5-flash-lite",
"source": "on_demand",
"created_at": "2026-10-06T10:00:00Z",
"updated_at": "2026-10-06T10:00:05Z"
}
}
}
FieldDescription
attachment_idThe file that was summarized (same id as in the message's attachments array).
message_id, conversation_idThe inbound message that carried the file and its conversation.
contact_id, channel_idThe contact who sent the file and the channel it arrived on. null if they could not be determined.
summary.statusAlways "ok" for this event.
summary.kind"image" or "pdf".
summary.textThe full plain-text summary.
summary.sectionsThe summary split into parts: description and text (extracted text) for images, summary and key_facts for PDFs. Empty if the text could not be split; use summary.text then.
summary.pages_read, summary.pages_total, summary.truncatedPDFs only: how many pages were read out of the total. truncated is true when only the first pages were read. null / false for images.
summary.modelThe AI model that produced the summary.
summary.source"agent" (read by a Conversational Agent) or "on_demand" (requested with Summarize).
summary.created_at, summary.updated_atWhen the summary was created and last updated (ISO 8601, UTC).
Treat summary.text as untrusted data

The summary describes a file your customer sent, including text copied out of it. Anyone can put any text into an image or PDF, so summary.text and summary.sections may contain misleading instructions, fake links, or HTML/script-like text. Store and display them as plain text only: do not render them as HTML or Markdown, do not turn URLs in them into clickable links, and do not pass them to an AI model as instructions. Summaries are AI-generated and may be inaccurate.

De-duplicate on event_id

event_id is the same for every delivery attempt of one summary, so use it to ignore retries.

If your webhook is limited to selected channels, this event is delivered only when its channel_id is one of them.

Message Fields Reference​

FieldTypeDescription
idstringUnique message ID
conversation_idstring | nullParent conversation ID. null for campaign and flow messages sent to a contact who has no conversation on that channel (campaigns never open a conversation; a flow message is attached to a conversation once the contact replies). Always present, never omitted.
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) and, on AI bot and agent replies, the bot metadata and source provenance; on a WhatsApp message.failed it can carry Meta's original errors in last_failed_error (the failure text itself is in data.error, see message.failed). 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.

Bot and agent message metadata​

Outbound messages written by an AI bot carry extra keys in meta, both in webhook payloads (message.sent, message.delivered, message.read, message.failed) and in the Messages API:

KeyTypeDescription
bot_messagebooleantrue on messages sent by a bot.
bot_namestringDisplay name (or name) of the bot that sent the message.
ai_generatedbooleantrue on messages sent by an AI bot or agent.
agent_idstringSet on Conversational Agent replies that cite sources: the agent that answered.
provenancearrayThe sources the answer was based on, at most 8: [{"type": "kb" | "web" | "faq", "title": "...", "url": "...", "document_id": "..."}]. url and document_id are present when known.
"meta": {
"bot_message": true,
"bot_name": "Support Agent",
"ai_generated": true,
"agent_id": "8c2f...",
"provenance": [
{"type": "kb", "title": "Return policy", "document_id": "d41e..."},
{"type": "web", "title": "Shipping times", "url": "https://acme.example/shipping"}
]
}

provenance and agent_id are present only when the reply has sources. They are read-only: if you send a message with these keys in meta via POST /messages, they are removed, so a message you send can never pose as a cited AI answer.

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 2 hours after the event was created
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 2 hours after the event was created, which covers the automatic retries. Deliveries that arrive later (replayed after a suspension, or retried manually) can carry an expired signed_url; use url then.

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.

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.

Reply and interaction context​

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).

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.assigned​

Triggered exactly once each time a conversation gets a new assignee: an agent assigns or reassigns it (inbox or conversation edit), an API call does, an automation such as a routing rule or bot hand-off does, a Flow's Open Conversation step does, or a conversation is created already assigned (for example a Live Chat session an agent picks up). Assigning the same user again sends nothing, and unassigning never sends this event.

{
"id": "evt_5c1e9a0b7d2f4e61",
"type": "conversation.assigned",
"event_id": "3f6d2c1a-8b4e-5f7a-9c0d-1e2f3a4b5c6d",
"created_at": "2026-03-04T10:31:02Z",
"tenant_id": "tenant_abc123",
"data": {
"conversation": {
"id": "conv_xyz789",
"channel_id": "ch_123",
"channel_type": "whatsapp",
"contact_id": "ct_456",
"assigned_user_id": "usr_alex",
"status": "assigned",
"subject": null,
"bot_session_id": null,
"tags": [
{ "id": "tag_vip", "name": "VIP", "color": "#F59E0B" }
],
"created_at": "2026-03-04T10:29:00Z",
"updated_at": "2026-03-04T10:31:02Z",
"last_customer_message_at": "2026-03-04T10:29:00Z",
"last_agent_reply_at": null,
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": []
}
},
"contact": {
"id": "ct_456",
"name": "John Doe",
"email": "[email protected]",
"phone": "+1234567890",
"contact_methods": []
},
"assigned_user_id": "usr_alex",
"assigned_to": {
"id": "usr_alex",
"name": "Alex Agent",
"email": "[email protected]"
},
"assigned_by": null,
"previous_assigned_user_id": "usr_sam",
"assignment_source": "flow",
"flow_id": "flow_42",
"flow_run_id": "run_9001"
}
}
FieldTypeDescription
event_id (envelope)stringUnique per assignment. Reassigning the same conversation produces a new event_id, so every assignment is delivered; retries of one delivery keep the same value.
conversationobjectThe conversation after the assignment, including tags[] (id, name, color) and an embedded contact.
contactobjectThe contact at the top level of data, same shape as on other events, including contact_methods[].
assigned_user_idstringThe new assignee's user ID. Always set.
assigned_toobjectThe new assignee: id, name, email.
assigned_byobject | nullThe team member who made the assignment (id, name, email). null when no person made it by hand: API calls, automations, bot hand-offs and Flows.
previous_assigned_user_idstring | nullThe assignee before this change. null when the conversation was unassigned or newly created.
assignment_sourcestringmanual (a team member in the app), api (API key, OAuth app or integration), automation (routing rules, bot hand-offs), or flow (a Flow's Open Conversation step). Treat as an open enum.
flow_idstring | nullThe Flow that made the assignment when assignment_source is flow, otherwise null.
flow_run_idstring | nullThe Flow run that made the assignment when assignment_source is flow, otherwise null.

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,
"owner_user_id": "usr_7f3a9c",
"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"
}
}
}

data.contact.owner_user_id is the ID of the contact's owner (team member), or null. Changing the owner (in the app, by API, bulk action, CSV import or when a team member is removed) sends a contact.updated.

WhatsApp number changes​

When a customer moves their WhatsApp account to a new phone number, SendSeven adds the new number to the same contact, makes it the primary WhatsApp ID and phone, keeps the old number as a superseded method, and sends one contact.updated with an extra data.whatsapp_number_change object:

"whatsapp_number_change": { "from": "+27821110000", "to": "+27832220000" }

The contact's contact_methods[] then lists the new number as primary. The old number is no longer used for sending, and subscriptions are not copied to the new number.

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",
"attachment_count": 1,
"attachments": [
{
"filename": "invoice-january.pdf",
"size": 48213,
"url": "https://storage.googleapis.com/...",
"attachment_id": "9b3f1a8e-7c2d-4e5b-9f01-12a3b4c5d6e7"
}
],
"created_at": "2026-03-04T16:00:00Z"
}
}

Each entry in attachments carries an attachment_id — the same attachment ID used everywhere else in the API (for example the AI file summary endpoints). It is null for images embedded inline in the email body (signature logos and similar), which are not treated as separate files.

Inline images in body_html point at temporary signed links (valid for about an hour), like the url of each attachment. To show the email later, load it from the email thread endpoints, which return fresh links every time (see Attachments of received emails).

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.


WhatsApp Flow Events​

Events for WhatsApp Flows, the native forms that open inside WhatsApp. The field-by-field reference, including the write-back result and all health alert types, is in Responses and Webhooks.

whatsapp_flow.completed​

Triggered once when a contact submits a WhatsApp Flow, however it was sent (inbox, campaign, API or automation). event_id is the flow session id, so you can de-duplicate on it.

{
"id": "evt_8f2a1c3b4d5e6f70",
"type": "whatsapp_flow.completed",
"event_id": "5b0e2c1a-7d3f-4e2b-9a01-3c4d5e6f7a8b",
"created_at": "2026-10-03T09:17:03Z",
"tenant_id": "tenant_abc123",
"data": {
"session_id": "5b0e2c1a-7d3f-4e2b-9a01-3c4d5e6f7a8b",
"flow_definition_id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f",
"flow_name": "Rückruf anfordern",
"version_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"meta_flow_id": "1234567890123456",
"channel_id": "8c1d0f5e-2a3b-4c5d-9e6f-7a8b9c0d1e2f",
"contact_id": "c0ffee00-1234-4abc-8def-567890abcdef",
"conversation_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"message_id": "msg_91ab2c3d",
"sender_type": "api",
"answers": {
"first_name": "Anna",
"callback_time": "vormittags",
"newsletter": true
},
"writeback": {
"state": "done",
"contact_fields": { "first_name": "Anna" },
"subscriptions": [{ "list_id": "a9c4e2f1-…", "status": "ok" }]
},
"completed_at": "2026-10-03T09:17:02Z"
}
}

When the Flow has photo or document answers, data.media lists the uploaded files with their SendSeven attachment id, a permanent url (API key with messages:read) and a 2-hour signed_url. Files that could not be stored only carry their status. See Uploaded files for the full shape.

answers holds every input by its name. Answers of fields the flow marks as sensitive are masked as ••••, the same as in the sessions API. If you need those values, collect them with your own data source webhook. The other answers are still personal data, so handle the payload accordingly. writeback.state is done, partial, error or skipped.

The contact's reply also arrives as a normal message.received event: an attachments entry of type flow_response with the answers in response_data, and a readable summary in text. For flows built in SendSeven, sensitive answers are masked as •••• there as well, in response_data and in text. Flows built outside SendSeven have no sensitive list we can read, so their answers arrive unchanged. In the SendSeven inbox, agents still see the real values.

whatsapp_flow.status_changed​

Triggered when WhatsApp changes the status of a flow you pushed or published, for example DRAFT to PUBLISHED, or PUBLISHED to BLOCKED or THROTTLED.

{
"id": "evt_2c4e6a8b0d1f3a5c",
"type": "whatsapp_flow.status_changed",
"event_id": "3c9e1f2a-4b5c-4d6e-8f70-1a2b3c4d5e6f:9b1f0c2d3e4a5b6c",
"created_at": "2026-10-03T10:02:11Z",
"tenant_id": "tenant_abc123",
"data": {
"channel_id": "8c1d0f5e-2a3b-4c5d-9e6f-7a8b9c0d1e2f",
"waba_id": "102938475610293",
"flow": { "id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "name": "Terminbuchung", "meta_flow_id": "1234567890123456" },
"message": "Flow Terminbuchung changed status from PUBLISHED to THROTTLED",
"old_status": "PUBLISHED",
"new_status": "THROTTLED"
}
}

whatsapp_flow.health_changed​

Triggered when WhatsApp raises or clears a health alert for a flow's data endpoint (dynamic flows): error rate, latency or availability.

{
"id": "evt_9d7b5f3a1c2e4a6b",
"type": "whatsapp_flow.health_changed",
"event_id": "3c9e1f2a-4b5c-4d6e-8f70-1a2b3c4d5e6f:4e7a2b9c0d1f3e5a",
"created_at": "2026-10-03T10:05:40Z",
"tenant_id": "tenant_abc123",
"data": {
"channel_id": "8c1d0f5e-2a3b-4c5d-9e6f-7a8b9c0d1e2f",
"waba_id": "102938475610293",
"flow": { "id": "0f9e8d7c-6b5a-4c3d-8e2f-1a0b9c8d7e6f", "name": "Terminbuchung", "meta_flow_id": "1234567890123456" },
"message": "Endpoint latency is above the threshold",
"alert": { "event": "ENDPOINT_LATENCY", "alert_state": "ACTIVATED", "alert_severity": "WARNING", "p90_latency": 7200, "threshold": 7000 },
"health_status": "RED"
}
}
event_id of lifecycle events

For whatsapp_flow.status_changed and whatsapp_flow.health_changed, event_id is <publication id>:<hash>. It is unique per change reported by WhatsApp, and stays the same if WhatsApp delivers the same change twice, so you can de-duplicate on it. The part before the colon is the publication id.


Agent Events​

Events for Conversational Agents. They fire only for skills with a goal whose webhook action is turned on (actions.webhook.enabled).

agent.goal_completed​

Triggered when an agent has collected and validated every required answer of a skill goal. It fires once per goal activation, in live conversations only (never from the test console).

{
"id": "evt_4f9c2a7b1d3e5f60",
"type": "agent.goal_completed",
"created_at": "2026-10-05T10:00:01Z",
"tenant_id": "990e8400-e29b-41d4-a716-446655440000",
"event_id": "6f1c1d1e-2b3a-4c5d-8e9f-0a1b2c3d4e5f",
"data": {
"agent": {
"id": "a1b2c3d4-0000-4000-8000-000000000001",
"name": "Sales Assistant"
},
"skill": {
"id": "b2c3d4e5-0000-4000-8000-000000000002",
"slug": "qualify-lead",
"version": 3
},
"conversation_id": "770e8400-e29b-41d4-a716-446655440000",
"contact_id": "880e8400-e29b-41d4-a716-446655440000",
"channel_type": "whatsapp",
"outputs": {
"company_size": "11-50",
"budget": 5000,
"topics": ["pricing", "integrations"],
"website": null
},
"saved_fields": ["company_size", "budget"],
"completed_at": "2026-10-05T10:00:00Z",
"flow_run_id": null
}
}
FieldDescription
agentThe agent that completed the goal (id, name).
skillThe skill and the skill version whose goal was completed.
conversation_id, contact_id, channel_typeWhere the goal was completed.
outputsEvery output key of the goal. Values are validated against the output type: text, number, boolean, date, a single option, or a list of options for multi-select outputs. An optional output the contact skipped is null.
saved_fieldsThe output keys that were written to contact fields.
completed_atWhen the goal was completed (ISO 8601, UTC).
flow_run_idThe Flow run that started the agent (Flow Run Assistant step), or null when the agent answered a conversation directly.
De-duplicate on event_id

event_id is the same for every delivery attempt of one goal completion, so use it to ignore retries.

When the goal is completed inside a Flow, the same answers are also available to the following Flow steps as {{vars.<prefix>.<key>}}.


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
attachment.summarizedAI summary of an image or PDF a contact sent is ready (agent read or on-demand Summarize). Only while AI features are enabled. summary.text is untrusted customer-file content.
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)
whatsapp_flow.completedA contact submitted a WhatsApp Flow (answers + write-back result)
whatsapp_flow.status_changedWhatsApp changed a flow's status (e.g. published, blocked, throttled)
whatsapp_flow.health_changedWhatsApp raised or cleared a health alert for a flow's data endpoint
agent.goal_completedA Conversational Agent completed a skill goal (validated answers + saved contact fields)
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.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​