Skip to main content

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 message
  • imageMessage - Image
  • videoMessage - Video
  • audioMessage - Audio/Voice note
  • documentMessage - Document
  • stickerMessage - Sticker
  • contactMessage - Contact card
  • locationMessage - Location
  • extendedTextMessage - 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 queued
  • sent - Sent to server
  • delivered - Delivered to recipient
  • read - 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 call
  • accept - Call accepted
  • reject - Call rejected
  • timeout - 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:

  1. Go to https://requestbin.com/
  2. Create a new bin
  3. Use the bin URL as your webhook
  4. 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

  1. Check webhook URL is accessible from the internet
  2. Verify URL returns 200 OK
  3. Check firewall settings
  4. 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