Skip to main content

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:

HeaderExampleDescription
Content-Typeapplication/jsonAlways JSON
X-Webhook-Eventmessage.receivedThe event type
X-Webhook-Delivery-Ida1b2c3d4-...Unique delivery ID (for deduplication)
X-Webhook-Secretwhsec_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

FieldTypeDescription
message_idstringPlatform message ID
tostringRecipient identifier (phone number, PSID, etc.)
typestringtext, image, template, interactive, etc.
contentobjectMessage content (varies by type)
statusstringsent
timestampstringISO 8601 timestamp
sender_idstring|nullUser ID of the human agent who sent it, or null
is_aibooleantrue if sent by an AI agent (only present on AI replies)
agent_idstringAI agent ID (only present on AI replies)
channelstringwhatsapp, 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

CodeDescription
131047Outside 24-hour conversation window
131026Message undeliverable (blocked or phone off)
131051Unsupported message type
131053Media download failed
130472Rate 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

ValueDescription
agentClosed manually by a team member
autoClosed automatically after idle timeout
systemClosed 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")