Webhook Events
SuperWaba sends real-time HTTP POST requests to your endpoint when events occur. This page documents every event type with full request/response examples.
See the Webhooks integration guide for setup instructions.
Common envelope
Every webhook delivery follows this structure:
{
"event": "event.name",
"timestamp": "2026-05-16T12:00:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
// Event-specific payload — see each event below
}
}
Headers
Every request includes these headers:
| Header | Example | Description |
|---|---|---|
Content-Type | application/json | Always JSON |
X-Webhook-Event | message.received | The event type |
X-Webhook-Delivery-Id | a1b2c3d4-... | Unique delivery ID (for deduplication) |
X-Webhook-Secret | whsec_abc123... | Your webhook secret (if configured) |
Expected response
Your endpoint should return a 2xx status code within 10 seconds. Any non-2xx response is treated as a failure.
HTTP/1.1 200 OK
message.received
Fired when a new incoming message arrives from a customer on any channel.
Payload
{
"event": "message.received",
"timestamp": "2026-05-16T12:00:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI0",
"from": "+918245678901",
"contact_name": "John Doe",
"type": "text",
"content": {
"text": "Hi, I need help with my order"
},
"timestamp": "2026-05-16T12:00:00Z"
}
}
Content variations by message type
Text message:
{
"type": "text",
"content": {
"text": "Hello, can you help me?"
}
}
Image message:
{
"type": "image",
"content": {
"image": {
"id": "media-id-123",
"mime_type": "image/jpeg",
"caption": "Here is the damaged product"
}
}
}
Document message:
{
"type": "document",
"content": {
"document": {
"id": "media-id-456",
"mime_type": "application/pdf",
"filename": "invoice.pdf"
}
}
}
Audio message:
{
"type": "audio",
"content": {
"audio": {
"id": "media-id-789",
"mime_type": "audio/ogg"
}
}
}
Video message:
{
"type": "video",
"content": {
"video": {
"id": "media-id-012",
"mime_type": "video/mp4"
}
}
}
Location message:
{
"type": "location",
"content": {
"location": {
"latitude": 25.2048,
"longitude": 55.2708,
"name": "Dubai Mall",
"address": "Financial Centre Rd, Dubai"
}
}
}
Interactive button reply:
{
"type": "interactive",
"content": {
"interactive": {
"type": "button_reply",
"button_reply": {
"id": "btn_yes",
"title": "Yes, confirm"
}
}
}
}
Interactive list reply:
{
"type": "interactive",
"content": {
"interactive": {
"type": "list_reply",
"list_reply": {
"id": "product_123",
"title": "Red T-Shirt",
"description": "Cotton, Size M"
}
}
}
}
Example: handling in Node.js
app.post('/webhook', (req, res) => {
const { event, data } = req.body;
if (event === 'message.received') {
console.log(`New message from ${data.contact_name} (${data.from})`);
console.log(`Type: ${data.type}`);
if (data.type === 'text') {
console.log(`Text: ${data.content.text}`);
}
// Process the message...
}
res.status(200).send('OK');
});
message.sent
Fired when a message is sent to a customer — either by a human agent, AI agent, or via the API.
Payload
{
"event": "message.sent",
"timestamp": "2026-05-16T12:00:05.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI1",
"to": "+918245678901",
"type": "text",
"content": {
"text": "Hi John! I'd be happy to help you with your order. Could you share your order number?"
},
"status": "sent",
"timestamp": "2026-05-16T12:00:05.000Z",
"sender_id": "agent-uuid-or-null",
"is_ai": true,
"agent_id": "ai-agent-uuid"
}
}
Fields
| Field | Type | Description |
|---|---|---|
message_id | string | Platform message ID |
to | string | Recipient identifier (phone number, PSID, etc.) |
type | string | text, image, template, interactive, etc. |
content | object | Message content (varies by type) |
status | string | sent |
timestamp | string | ISO 8601 timestamp |
sender_id | string|null | User ID of the human agent who sent it, or null |
is_ai | boolean | true if sent by an AI agent (only present on AI replies) |
agent_id | string | AI agent ID (only present on AI replies) |
channel | string | whatsapp, instagram, or messenger (present on Instagram/Messenger) |
Template message example
{
"event": "message.sent",
"timestamp": "2026-05-16T14:30:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI2",
"to": "+918245678901",
"type": "template",
"content": {
"template": {
"name": "order_shipped",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "John" },
{ "type": "text", "text": "ORD-78901" }
]
}
]
}
},
"status": "sent",
"timestamp": "2026-05-16T14:30:00.000Z",
"sender_id": "user-uuid"
}
}
message.delivered
Fired when a sent message is delivered to the recipient's device.
Payload
{
"event": "message.delivered",
"timestamp": "2026-05-16T12:00:08.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI1",
"status": "delivered",
"timestamp": "2026-05-16T12:00:08Z",
"recipient_id": "+918245678901"
}
}
message.read
Fired when the recipient reads the message (blue double-check on WhatsApp).
Payload
{
"event": "message.read",
"timestamp": "2026-05-16T12:01:15.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI1",
"status": "read",
"timestamp": "2026-05-16T12:01:15Z",
"recipient_id": "+918245678901"
}
}
message.failed
Fired when a message fails to deliver.
Payload
{
"event": "message.failed",
"timestamp": "2026-05-16T12:00:10.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"message_id": "wamid.HBgNOTE4MjQ1Njc4OTAxFQIAERgSQzI3",
"status": "failed",
"timestamp": "2026-05-16T12:00:10Z",
"recipient_id": "+918245678901",
"error": {
"code": 131047,
"message": "Re-engagement message - Message failed to send because more than 24 hours have passed since the customer last replied to this number"
}
}
}
Common error codes
| Code | Description |
|---|---|
131047 | Outside 24-hour conversation window |
131026 | Message undeliverable (blocked or phone off) |
131051 | Unsupported message type |
131053 | Media download failed |
130472 | Rate limit exceeded |
contact.created
Fired when a new contact is created (first message from a new customer).
Payload
{
"event": "contact.created",
"timestamp": "2026-05-16T12:00:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"contact_id": "uuid",
"phone": "+918245678901",
"name": "John Doe",
"channel": "whatsapp",
"created_at": "2026-05-16T12:00:00.000Z"
}
}
contact.updated
Fired when a contact's details are modified.
Payload
{
"event": "contact.updated",
"timestamp": "2026-05-16T14:30:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"contact_id": "uuid",
"phone": "+918245678901",
"name": "John D.",
"labels": ["vip", "returning-customer"],
"updated_at": "2026-05-16T14:30:00.000Z"
}
}
conversation.opened
Fired when a new conversation is created (first message from a new or returning customer).
Payload
{
"event": "conversation.opened",
"timestamp": "2026-05-16T12:00:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"conversation_id": "uuid",
"contact_id": "uuid",
"contact_phone": "+918245678901",
"contact_name": "John Doe",
"channel": "whatsapp",
"is_ai_handling": true,
"ai_agent_id": "uuid"
}
}
conversation.closed
Fired when a conversation is closed (manually by an agent, or automatically by the idle auto-close system).
Payload
{
"event": "conversation.closed",
"timestamp": "2026-05-16T18:00:00.000Z",
"webhook_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"data": {
"conversation_id": "uuid",
"contact_id": "uuid",
"channel": "whatsapp",
"closed_by": "auto",
"summary": "Customer inquired about order status for ORD-12345. AI agent provided tracking information.",
"intent_tag": "support",
"sentiment": "positive",
"message_count": 8,
"duration_minutes": 12
}
}
closed_by values
| Value | Description |
|---|---|
agent | Closed manually by a team member |
auto | Closed automatically after idle timeout |
system | Closed by the system (e.g., conversation limit reached) |
Full example: cURL test
You can test your webhook endpoint with this cURL command:
curl -X POST https://your-endpoint.com/webhook \
-H "Content-Type: application/json" \
-H "X-Webhook-Event: message.received" \
-H "X-Webhook-Delivery-Id: test-$(uuidgen)" \
-H "X-Webhook-Secret: your-secret-here" \
-d '{
"event": "message.received",
"timestamp": "2026-05-16T12:00:00.000Z",
"webhook_id": "test-webhook-id",
"data": {
"message_id": "wamid.test123",
"from": "+918245678901",
"contact_name": "Test Customer",
"type": "text",
"content": {
"text": "Hello, I need help!"
},
"timestamp": "2026-05-16T12:00:00Z"
}
}'
Full example: Express.js webhook server
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
// Verify webhook signature
function verifySignature(req: express.Request): boolean {
const secret = req.headers['x-webhook-secret'] as string;
return secret === WEBHOOK_SECRET;
}
app.post('/webhook', (req, res) => {
// 1. Verify signature
if (!verifySignature(req)) {
return res.status(401).send('Invalid signature');
}
const { event, data, timestamp } = req.body;
const deliveryId = req.headers['x-webhook-delivery-id'];
console.log(`[${timestamp}] Event: ${event} (delivery: ${deliveryId})`);
// 2. Handle events
switch (event) {
case 'message.received':
console.log(`Message from ${data.contact_name}: ${data.content?.text}`);
// Save to your database, forward to Slack, trigger a workflow, etc.
break;
case 'message.sent':
console.log(`Message sent to ${data.to}: ${data.content?.text}`);
if (data.is_ai) {
console.log(` (sent by AI agent ${data.agent_id})`);
}
break;
case 'message.delivered':
console.log(`Message ${data.message_id} delivered to ${data.recipient_id}`);
break;
case 'message.read':
console.log(`Message ${data.message_id} read by ${data.recipient_id}`);
break;
case 'message.failed':
console.error(`Message ${data.message_id} failed: ${data.error?.message}`);
// Alert your team, retry logic, etc.
break;
case 'contact.created':
console.log(`New contact: ${data.name} (${data.phone})`);
// Sync to your CRM
break;
case 'conversation.opened':
console.log(`New conversation with ${data.contact_name}`);
break;
case 'conversation.closed':
console.log(`Conversation closed: ${data.summary}`);
break;
default:
console.log(`Unhandled event: ${event}`);
}
// 3. Always return 200
res.status(200).json({ received: true });
});
app.listen(3000, () => {
console.log('Webhook server listening on port 3000');
});
Full example: Python Flask webhook server
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
@app.route("/webhook", methods=["POST"])
def webhook():
# Verify secret
if request.headers.get("X-Webhook-Secret") != WEBHOOK_SECRET:
return "Unauthorized", 401
payload = request.json
event = payload.get("event")
data = payload.get("data", {})
if event == "message.received":
print(f"Message from {data.get('contact_name')}: {data.get('content', {}).get('text')}")
elif event == "message.sent":
print(f"Message sent to {data.get('to')}")
elif event == "message.failed":
error = data.get("error", {})
print(f"Message failed: {error.get('code')} - {error.get('message')}")
elif event == "conversation.closed":
print(f"Conversation closed: {data.get('summary')}")
return jsonify({"received": True}), 200
if __name__ == "__main__":
app.run(port=3000)
Event lifecycle diagram
A typical WhatsApp conversation triggers events in this order:
Customer sends first message
→ contact.created
→ conversation.opened
→ message.received
AI agent replies
→ message.sent (is_ai: true)
→ message.delivered
→ message.read
Customer replies again
→ message.received
Human agent takes over and replies
→ message.sent (sender_id: "user-uuid")
→ message.delivered
Conversation closes after idle timeout
→ conversation.closed (closed_by: "auto")