Webhooks
Webhooks allow you to receive real-time notifications about events happening in your WhatsApp sessions.
Setting Up Webhooks
Configure your webhook URL when creating a session:
curl -X POST http://localhost:3000/sessions/create \
-H "Content-Type: application/json" \
-d '{
"name": "my-session",
"webhookUrl": "https://your-domain.com/webhook"
}'
Your webhook endpoint will receive POST requests with JSON payloads for various events.
Webhook Events
Connection Events
qr_ready
QR code is ready to be scanned.
{
"event": "qr_ready",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"timestamp": "2026-02-01T12:00:00.000Z"
}
authenticated
Session successfully authenticated (QR code scanned).
{
"event": "authenticated",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"phoneNumber": "919988066776",
"timestamp": "2026-02-01T12:00:00.000Z"
}
ready
Session is ready to send/receive messages.
{
"event": "ready",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"phoneNumber": "919988066776",
"timestamp": "2026-02-01T12:00:00.000Z"
}
disconnected
Session disconnected from WhatsApp.
{
"event": "disconnected",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"error": "Connection lost",
"willReconnect": true,
"timestamp": "2026-02-01T12:00:00.000Z"
}
logged_out
User logged out, session will not reconnect.
{
"event": "logged_out",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"timestamp": "2026-02-01T12:00:00.000Z"
}
Message Events
message
New message received.
{
"event": "message",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"message": {
"id": "3EB0ABC123456789",
"from": "919988066776@s.whatsapp.net",
"to": "919878695378@s.whatsapp.net",
"body": "Hello!",
"type": "conversation",
"timestamp": 1706774400,
"hasMedia": false,
"fromMe": false,
"isGroup": false
},
"timestamp": "2026-02-01T12:00:00.000Z"
}
Message Types:
conversation- Text messageimageMessage- ImagevideoMessage- VideoaudioMessage- Audio/Voice notedocumentMessage- DocumentstickerMessage- StickercontactMessage- Contact cardlocationMessage- LocationextendedTextMessage- Text with link preview
message_status
Message delivery status update.
{
"event": "message_status",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"statusUpdate": {
"messageId": "3EB0ABC123456789",
"chatId": "919988066776@s.whatsapp.net",
"status": "delivered",
"timestamp": 1706774400
},
"timestamp": "2026-02-01T12:00:00.000Z"
}
Status Values:
pending- Message queuedsent- Sent to serverdelivered- Delivered to recipientread- Read by recipient
message_reaction
Someone reacted to a message.
{
"event": "message_reaction",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"messageId": "3EB0ABC123456789",
"chatId": "919988066776@s.whatsapp.net",
"from": "919988066776@s.whatsapp.net",
"reaction": "❤️",
"timestamp": 1706774400
}
message_revoke_everyone
Message deleted for everyone.
{
"event": "message_revoke_everyone",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"messageId": "3EB0ABC123456789",
"chatId": "919988066776@s.whatsapp.net",
"fromMe": false,
"timestamp": 1706774400
}
Group Events
group_join
Participant(s) joined a group.
{
"event": "group_join",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"groupId": "120363123456789@g.us",
"action": "add",
"participants": ["919988066776@s.whatsapp.net"],
"timestamp": 1706774400
}
group_leave
Participant(s) left a group.
{
"event": "group_leave",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"groupId": "120363123456789@g.us",
"action": "remove",
"participants": ["919988066776@s.whatsapp.net"],
"timestamp": 1706774400
}
group_update
Group settings updated.
{
"event": "group_update",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"groupId": "120363123456789@g.us",
"updates": {
"subject": "New Group Name",
"desc": "New description",
"announce": true
},
"timestamp": 1706774400
}
Call Events
call
Incoming or outgoing call.
{
"event": "call",
"sessionId": "e67a00be-ed45-4356-9488-049cabb9895d",
"callId": "call_id_123",
"from": "919988066776@s.whatsapp.net",
"status": "offer",
"isVideo": false,
"isGroup": false,
"timestamp": 1706774400
}
Call Status:
offer- Incoming callaccept- Call acceptedreject- Call rejectedtimeout- Call timed out
Implementing Webhook Handlers
PHP Example
<?php
// webhook.php
// Get the raw POST data
$payload = json_decode(file_get_contents('php://input'), true);
if (!$payload) {
http_response_code(400);
exit('Invalid JSON');
}
$event = $payload['event'] ?? null;
$sessionId = $payload['sessionId'] ?? null;
// Log the event
error_log("Webhook received: $event for session $sessionId");
switch ($event) {
case 'message':
handleIncomingMessage($payload);
break;
case 'message_status':
handleMessageStatus($payload);
break;
case 'ready':
handleSessionReady($payload);
break;
default:
error_log("Unhandled event: $event");
}
// Always respond with 200 OK
http_response_code(200);
echo json_encode(['success' => true]);
function handleIncomingMessage($payload) {
$message = $payload['message'];
$from = $message['from'];
$body = $message['body'];
// Store in database
// Process message
// Send auto-reply if needed
error_log("Message from $from: $body");
}
function handleMessageStatus($payload) {
$status = $payload['statusUpdate'];
$messageId = $status['messageId'];
$statusValue = $status['status'];
// Update message status in database
error_log("Message $messageId status: $statusValue");
}
function handleSessionReady($payload) {
$sessionId = $payload['sessionId'];
$phoneNumber = $payload['phoneNumber'];
// Update session status in database
error_log("Session $sessionId ready with number $phoneNumber");
}
?>
Node.js Example
// webhook.js
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhook', async (req, res) => {
const { event, sessionId, ...data } = req.body;
console.log(`Webhook received: ${event} for session ${sessionId}`);
try {
switch (event) {
case 'message':
await handleIncomingMessage(data);
break;
case 'message_status':
await handleMessageStatus(data);
break;
case 'ready':
await handleSessionReady(data);
break;
default:
console.log(`Unhandled event: ${event}`);
}
// Always respond with 200 OK
res.json({ success: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
async function handleIncomingMessage(data) {
const { message } = data;
const { from, body, type } = message;
console.log(`Message from ${from}: ${body}`);
// Store in database
// Process message
// Send auto-reply if needed
}
async function handleMessageStatus(data) {
const { statusUpdate } = data;
const { messageId, status } = statusUpdate;
console.log(`Message ${messageId} status: ${status}`);
// Update message status in database
}
async function handleSessionReady(data) {
const { phoneNumber } = data;
console.log(`Session ready with number ${phoneNumber}`);
// Update session status in database
}
app.listen(3001, () => {
console.log('Webhook server listening on port 3001');
});
Python Example
# webhook.py
from flask import Flask, request, jsonify
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_json()
if not payload:
return jsonify({'error': 'Invalid JSON'}), 400
event = payload.get('event')
session_id = payload.get('sessionId')
logging.info(f"Webhook received: {event} for session {session_id}")
try:
if event == 'message':
handle_incoming_message(payload)
elif event == 'message_status':
handle_message_status(payload)
elif event == 'ready':
handle_session_ready(payload)
else:
logging.info(f"Unhandled event: {event}")
# Always respond with 200 OK
return jsonify({'success': True}), 200
except Exception as e:
logging.error(f"Webhook error: {e}")
return jsonify({'error': 'Internal server error'}), 500
def handle_incoming_message(payload):
message = payload.get('message', {})
from_number = message.get('from')
body = message.get('body')
logging.info(f"Message from {from_number}: {body}")
# Store in database
# Process message
# Send auto-reply if needed
def handle_message_status(payload):
status_update = payload.get('statusUpdate', {})
message_id = status_update.get('messageId')
status = status_update.get('status')
logging.info(f"Message {message_id} status: {status}")
# Update message status in database
def handle_session_ready(payload):
phone_number = payload.get('phoneNumber')
logging.info(f"Session ready with number {phone_number}")
# Update session status in database
if __name__ == '__main__':
app.run(host='0.0.0.0', port=3001)
Best Practices
1. Respond Quickly
Always respond with 200 OK within 5 seconds:
// Respond immediately
http_response_code(200);
echo json_encode(['success' => true]);
// Then process async
fastcgi_finish_request(); // PHP
// Process webhook data
2. Handle Duplicates
Use message IDs to prevent duplicate processing:
const processedMessages = new Set();
if (processedMessages.has(messageId)) {
return; // Already processed
}
processedMessages.add(messageId);
3. Secure Your Webhook
Add token verification:
$secret = 'your_secret_token';
$providedToken = $_SERVER['HTTP_X_WEBHOOK_TOKEN'] ?? '';
if ($providedToken !== $secret) {
http_response_code(403);
exit('Invalid token');
}
Configure in session creation:
{
"webhookUrl": "https://yourdomain.com/webhook?token=your_secret_token"
}
4. Use HTTPS
Always use HTTPS for webhook URLs to encrypt data in transit.
5. Log Everything
Keep detailed logs for debugging:
console.log(JSON.stringify(payload, null, 2));
6. Implement Retry Logic
Handle temporary failures gracefully:
import time
def process_webhook(payload, max_retries=3):
for attempt in range(max_retries):
try:
# Process webhook
return True
except Exception as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt) # Exponential backoff
else:
logging.error(f"Failed after {max_retries} attempts: {e}")
return False
Testing Webhooks Locally
Using ngrok
# Install ngrok
# Download from https://ngrok.com/
# Start your webhook server
python webhook.py
# In another terminal, start ngrok
ngrok http 3001
# Use the ngrok URL as your webhook
# Example: https://abc123.ngrok.io/webhook
Using Request Bin
For testing without writing code:
- Go to https://requestbin.com/
- Create a new bin
- Use the bin URL as your webhook
- View incoming webhooks in real-time
Webhook Security Checklist
- ✅ Use HTTPS URLs only
- ✅ Implement token-based authentication
- ✅ Validate JSON payload structure
- ✅ Respond with 200 OK quickly
- ✅ Process webhooks asynchronously
- ✅ Log all webhook events
- ✅ Handle duplicate events
- ✅ Implement rate limiting
- ✅ Monitor webhook failures
- ✅ Set up alerts for errors
Troubleshooting
Webhook Not Receiving Events
- Check webhook URL is accessible from the internet
- Verify URL returns 200 OK
- Check firewall settings
- Review server logs for errors
Duplicate Events
- Use message IDs to deduplicate
- Implement idempotent processing
- Store processed event IDs
Timeout Errors
- Respond with 200 OK immediately
- Process webhook data asynchronously
- Optimize database queries
Missing Events
- Check webhook endpoint is always available
- Implement retry logic
- Monitor uptime