Skip to main content

What are Webhooks?

Webhooks allow Evolution API to send real-time notifications to your application when events occur in WhatsApp. Instead of polling for updates, your application receives HTTP POST requests with event data as it happens. Evolution API provides:
  • 40+ event types covering messages, contacts, groups, and connection status
  • Automatic retry logic with exponential backoff
  • Custom headers for authentication
  • Event filtering to receive only the events you need
  • Global and per-instance webhook configurations
Webhooks are the recommended way to build responsive WhatsApp applications. They ensure zero message loss and real-time user experiences.

Webhook Configuration

Global Webhooks

Configure webhooks for all instances via environment variables:
# Enable global webhooks
WEBHOOK_GLOBAL_ENABLED=true

# URL to receive all events
WEBHOOK_GLOBAL_URL='https://your-app.com/webhook'

# Send events to /webhook/{event-name} instead of /webhook
WEBHOOK_GLOBAL_WEBHOOK_BY_EVENTS=false
When WEBHOOK_BY_EVENTS=true, each event is sent to a unique URL:
  • Messages: https://your-app.com/webhook/messages-upsert
  • Contacts: https://your-app.com/webhook/contacts-upsert
  • Connection: https://your-app.com/webhook/connection-update

Per-Instance Webhooks

Configure webhooks when creating an instance:
curl -X POST https://your-api.com/instance/create \
  -H "apikey: YOUR_GLOBAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "instanceName": "my-whatsapp",
    "webhook": {
      "enabled": true,
      "url": "https://your-app.com/webhook/instance1",
      "byEvents": false,
      "base64": true,
      "headers": {
        "Authorization": "Bearer your-secret-token",
        "X-Custom-Header": "custom-value"
      }
    }
  }'

Update Existing Instance Webhook

curl -X POST https://your-api.com/webhook/set/my-whatsapp \
  -H "apikey: YOUR_INSTANCE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook": {
      "enabled": true,
      "url": "https://your-app.com/webhook",
      "events": [
        "MESSAGES_UPSERT",
        "MESSAGES_UPDATE",
        "CONNECTION_UPDATE"
      ]
    }
  }'

Available Events

Evolution API supports over 40 webhook events. Here are the most important ones:

Instance Events

EventDescriptionTrigger
APPLICATION_STARTUPAPI server startedOn server boot
INSTANCE_CREATENew instance createdPOST /instance/create
INSTANCE_DELETEInstance deletedDELETE /instance/delete
QRCODE_UPDATEDNew QR code generatedDuring connection
CONNECTION_UPDATEConnection state changedConnect/disconnect
REMOVE_INSTANCEInstance auto-removedTimeout or error
LOGOUT_INSTANCEInstance logged outManual logout

Message Events

EventDescriptionTrigger
MESSAGES_SETHistorical messages loadedInitial sync
MESSAGES_UPSERTNew message received/sentReal-time messages
MESSAGES_UPDATEMessage status changedRead, delivered, played
MESSAGES_EDITEDMessage content editedEdit message
MESSAGES_DELETEMessage deletedDelete for everyone
SEND_MESSAGEMessage sent successfullyAfter sending
SEND_MESSAGE_UPDATESent message status updatedDelivery confirmation

Contact Events

EventDescriptionTrigger
CONTACTS_SETContacts loadedInitial sync
CONTACTS_UPSERTNew/updated contactContact change
CONTACTS_UPDATEContact info changedProfile update
PRESENCE_UPDATEUser presence changedOnline/offline/typing

Chat Events

EventDescriptionTrigger
CHATS_SETChats loadedInitial sync
CHATS_UPSERTNew/updated chatNew conversation
CHATS_UPDATEChat metadata changedMute, archive, pin
CHATS_DELETEChat deletedDelete conversation

Group Events

EventDescriptionTrigger
GROUPS_UPSERTNew/updated groupJoin/create group
GROUPS_UPDATEGroup info changedName, description, icon
GROUP_PARTICIPANTS_UPDATEParticipants changedAdd/remove members

Other Events

EventDescriptionTrigger
LABELS_EDITLabel modifiedEdit label
LABELS_ASSOCIATIONLabel assignedTag conversation
CALLIncoming callVoice/video call
TYPEBOT_STARTTypebot session startedBot triggered
TYPEBOT_CHANGE_STATUSTypebot status changedBot state change
ERRORSError occurredAny error

Configure Events in .env

# Enable specific events
WEBHOOK_EVENTS_QRCODE_UPDATED=true
WEBHOOK_EVENTS_MESSAGES_UPSERT=true
WEBHOOK_EVENTS_MESSAGES_UPDATE=true
WEBHOOK_EVENTS_CONNECTION_UPDATE=true
WEBHOOK_EVENTS_CONTACTS_UPSERT=true
WEBHOOK_EVENTS_GROUPS_UPSERT=true
WEBHOOK_EVENTS_CALL=true

# Disable unwanted events
WEBHOOK_EVENTS_MESSAGES_SET=false
WEBHOOK_EVENTS_CONTACTS_SET=false
WEBHOOK_EVENTS_CHATS_SET=false
Disable high-volume events like MESSAGES_SET, CONTACTS_SET, and CHATS_SET in production to reduce webhook traffic. These events fire during initial sync and can send thousands of payloads.

Webhook Payload Structure

All webhook requests follow this structure:
{
  "event": "MESSAGES_UPSERT",
  "instance": "my-whatsapp",
  "data": {
    // Event-specific data
  },
  "destination": "https://your-app.com/webhook",
  "date_time": "2026-03-04T12:34:56.789Z",
  "sender": "[email protected]",
  "server_url": "https://your-evolution-api.com",
  "apikey": "YOUR_INSTANCE_TOKEN"
}

Message Received Example

{
  "event": "MESSAGES_UPSERT",
  "instance": "my-whatsapp",
  "data": {
    "key": {
      "remoteJid": "[email protected]",
      "fromMe": false,
      "id": "3EB0C7B4E7A2B8E6D4F1"
    },
    "pushName": "John Doe",
    "message": {
      "conversation": "Hello, I need help with my order"
    },
    "messageType": "conversation",
    "messageTimestamp": 1709553296,
    "instanceId": "550e8400-e29b-41d4-a716-446655440000",
    "source": "ios"
  },
  "destination": "https://your-app.com/webhook",
  "date_time": "2026-03-04T12:34:56.789Z",
  "sender": "[email protected]",
  "server_url": "https://evolution-api.com",
  "apikey": "B5F6E890D1234567890ABCDEF1234567"
}

Connection Update Example

{
  "event": "CONNECTION_UPDATE",
  "instance": "my-whatsapp",
  "data": {
    "state": "open",
    "statusReason": 200
  },
  "destination": "https://your-app.com/webhook",
  "date_time": "2026-03-04T12:30:00.000Z",
  "server_url": "https://evolution-api.com",
  "apikey": "B5F6E890D1234567890ABCDEF1234567"
}

QR Code Updated Example

{
  "event": "QRCODE_UPDATED",
  "instance": "my-whatsapp",
  "data": {
    "qrcode": {
      "base64": "data:image/png;base64,iVBORw0KGgoAAAANS...",
      "code": "2@v3XyZ...",
      "count": 1
    }
  },
  "destination": "https://your-app.com/webhook",
  "date_time": "2026-03-04T12:29:45.123Z",
  "server_url": "https://evolution-api.com",
  "apikey": "B5F6E890D1234567890ABCDEF1234567"
}

Webhook Authentication

Secure your webhook endpoint using custom headers:

Bearer Token Authentication

{
  "webhook": {
    "enabled": true,
    "url": "https://your-app.com/webhook",
    "headers": {
      "Authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    }
  }
}
In your webhook handler:
app.post('/webhook', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (token !== process.env.WEBHOOK_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  
  // Process webhook
  const { event, instance, data } = req.body;
  console.log(`Received ${event} from ${instance}`);
  
  res.status(200).send('OK');
});

JWT Authentication

Evolution API can automatically generate JWT tokens for each request:
{
  "webhook": {
    "enabled": true,
    "url": "https://your-app.com/webhook",
    "headers": {
      "jwt_key": "your-secret-signing-key"
    }
  }
}
From the source code (src/api/integrations/event/webhook/webhook.controller.ts:80):
if (webhookHeaders && 'jwt_key' in webhookHeaders) {
  const jwtKey = webhookHeaders['jwt_key'];
  const jwtToken = this.generateJwtToken(jwtKey);
  webhookHeaders['Authorization'] = `Bearer ${jwtToken}`;
  
  delete webhookHeaders['jwt_key'];
}

private generateJwtToken(authToken: string): string {
  const payload = {
    iat: Math.floor(Date.now() / 1000),
    exp: Math.floor(Date.now() / 1000) + 600, // 10 min expiration
    app: 'evolution',
    action: 'webhook',
  };
  
  const token = jwt.sign(payload, authToken, { algorithm: 'HS256' });
  return token;
}
Verify in your handler:
const jwt = require('jsonwebtoken');

app.post('/webhook', (req, res) => {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
    if (decoded.app !== 'evolution' || decoded.action !== 'webhook') {
      throw new Error('Invalid token claims');
    }
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
  
  // Process webhook
  res.status(200).send('OK');
});

Retry Logic and Error Handling

Evolution API includes sophisticated retry logic to ensure reliable webhook delivery.

Retry Configuration

# Maximum retry attempts
WEBHOOK_RETRY_MAX_ATTEMPTS=10

# Initial delay before first retry (seconds)
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5

# Use exponential backoff (doubles delay each attempt)
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true

# Maximum delay between retries (seconds)
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300

# Random jitter factor (0.0 - 1.0)
WEBHOOK_RETRY_JITTER_FACTOR=0.2

# HTTP status codes that should NOT trigger retries
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422

# Request timeout (milliseconds)
WEBHOOK_REQUEST_TIMEOUT_MS=60000

How Retry Works

From src/api/integrations/event/webhook/webhook.controller.ts:203:
private async retryWebhookRequest(
  httpService: AxiosInstance,
  webhookData: any,
  origin: string,
  baseURL: string,
  serverUrl: string,
  maxRetries?: number,
  delaySeconds?: number,
): Promise<void> {
  const maxRetryAttempts = maxRetries ?? 10;
  const initialDelay = delaySeconds ?? 5;
  const useExponentialBackoff = true;
  const maxDelay = 300;
  const jitterFactor = 0.2;
  const nonRetryableStatusCodes = [400, 401, 403, 404, 422];

  let attempts = 0;

  while (attempts < maxRetryAttempts) {
    try {
      await httpService.post('', webhookData);
      if (attempts > 0) {
        this.logger.log(`Success after ${attempts + 1} attempts`);
      }
      return; // Success!
    } catch (error) {
      attempts++;

      // Don't retry on client errors (4xx)
      if (error?.response?.status && 
          nonRetryableStatusCodes.includes(error.response.status)) {
        this.logger.error(`Non-retryable error (${error.response.status})`);
        throw error;
      }

      if (attempts === maxRetryAttempts) {
        throw error; // Max attempts reached
      }

      // Calculate next delay with exponential backoff + jitter
      let nextDelay = initialDelay;
      if (useExponentialBackoff) {
        nextDelay = Math.min(
          initialDelay * Math.pow(2, attempts - 1),
          maxDelay
        );
        
        const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1);
        nextDelay = Math.max(initialDelay, nextDelay + jitter);
      }

      this.logger.log(`Waiting ${nextDelay.toFixed(1)}s before retry`);
      await new Promise(resolve => setTimeout(resolve, nextDelay * 1000));
    }
  }
}
With default settings, Evolution API will retry for up to 30+ minutes before giving up:
  • Attempt 1: Wait 5s
  • Attempt 2: Wait 10s
  • Attempt 3: Wait 20s
  • Attempt 10: Wait 300s (5 min)

Error Webhooks

Receive notifications when errors occur:
WEBHOOK_EVENTS_ERRORS=true
WEBHOOK_EVENTS_ERRORS_WEBHOOK=https://your-app.com/webhook/errors

Implementing a Webhook Handler

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook', async (req, res) => {
  try {
    const { event, instance, data, sender } = req.body;
    
    console.log(`[${instance}] Received ${event}`);
    
    switch (event) {
      case 'MESSAGES_UPSERT':
        await handleNewMessage(instance, data, sender);
        break;
        
      case 'CONNECTION_UPDATE':
        await handleConnectionUpdate(instance, data);
        break;
        
      case 'QRCODE_UPDATED':
        await handleQRCode(instance, data);
        break;
        
      default:
        console.log(`Unhandled event: ${event}`);
    }
    
    // Always respond with 200 to acknowledge receipt
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook error:', error);
    // Still return 200 to prevent retries for processing errors
    res.status(200).send('ERROR');
  }
});

async function handleNewMessage(instance, data, sender) {
  // Only process incoming messages
  if (data.key.fromMe) return;
  
  const message = data.message?.conversation || 
                  data.message?.extendedTextMessage?.text;
  
  console.log(`Message from ${sender}: ${message}`);
  
  // Process message (save to DB, trigger bot, etc.)
  await db.messages.create({
    instanceId: data.instanceId,
    sender: sender,
    text: message,
    timestamp: new Date(data.messageTimestamp * 1000)
  });
}

async function handleConnectionUpdate(instance, data) {
  console.log(`${instance} connection: ${data.state}`);
  
  if (data.state === 'open') {
    await db.instances.update(instance, { status: 'connected' });
  } else if (data.state === 'close') {
    await db.instances.update(instance, { status: 'disconnected' });
    // Alert admin
    await sendAlert(`Instance ${instance} disconnected`);
  }
}

async function handleQRCode(instance, data) {
  const { base64 } = data.qrcode;
  
  // Store QR code for display in UI
  await redis.set(`qrcode:${instance}`, base64, 'EX', 300); // 5 min TTL
  
  // Notify user via WebSocket
  io.to(instance).emit('qrcode', { base64 });
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Best Practices

Your webhook endpoint should return 200 OK as quickly as possible:
app.post('/webhook', async (req, res) => {
  // Acknowledge immediately
  res.status(200).send('OK');
  
  // Process asynchronously
  processWebhookAsync(req.body).catch(console.error);
});
If you return an error status, Evolution API will retry the webhook, potentially causing duplicates.
Handle duplicate webhooks gracefully using message IDs:
async function handleMessage(data) {
  const messageId = data.key.id;
  
  // Check if already processed
  const exists = await redis.get(`processed:${messageId}`);
  if (exists) {
    console.log('Duplicate webhook, skipping');
    return;
  }
  
  // Process message
  await processMessage(data);
  
  // Mark as processed (24 hour TTL)
  await redis.setex(`processed:${messageId}`, 86400, '1');
}
Only enable events you actually use to reduce webhook traffic:
{
  "webhook": {
    "enabled": true,
    "url": "https://your-app.com/webhook",
    "events": [
      "MESSAGES_UPSERT",       // New messages only
      "CONNECTION_UPDATE",     // Connection status
      "MESSAGES_UPDATE"        // Read receipts
    ]
  }
}
Avoid enabling MESSAGES_SET, CONTACTS_SET, and CHATS_SET unless you need historical sync.
For high-volume webhooks, use a queue to prevent blocking:
const Bull = require('bull');
const webhookQueue = new Bull('webhooks');

app.post('/webhook', (req, res) => {
  // Add to queue
  webhookQueue.add(req.body);
  res.status(200).send('OK');
});

// Process in background
webhookQueue.process(async (job) => {
  const { event, instance, data } = job.data;
  await processWebhook(event, instance, data);
});
Validate webhooks are from your Evolution API:
app.post('/webhook', (req, res) => {
  const serverUrl = req.body.server_url;
  const apikey = req.body.apikey;
  
  // Verify server URL
  if (serverUrl !== process.env.EVOLUTION_API_URL) {
    return res.status(403).send('Forbidden');
  }
  
  // Verify API key
  const validKey = await verifyAPIKey(apikey, req.body.instance);
  if (!validKey) {
    return res.status(401).send('Unauthorized');
  }
  
  // Process webhook
  res.status(200).send('OK');
});
Track webhook delivery and errors:
const metrics = {
  received: 0,
  processed: 0,
  errors: 0
};

app.post('/webhook', async (req, res) => {
  metrics.received++;
  
  try {
    await processWebhook(req.body);
    metrics.processed++;
  } catch (error) {
    metrics.errors++;
    console.error('Webhook error:', error);
  }
  
  res.status(200).send('OK');
});

// Expose metrics
app.get('/metrics', (req, res) => {
  res.json(metrics);
});

Testing Webhooks

Using ngrok for Local Development

1

Install ngrok

# Download from https://ngrok.com
npm install -g ngrok
2

Start Your Webhook Server

node webhook-server.js
# Listening on http://localhost:3000
3

Expose with ngrok

ngrok http 3000
Copy the HTTPS URL:
Forwarding  https://abc123.ngrok.io -> http://localhost:3000
4

Configure Webhook

curl -X POST https://your-api.com/webhook/set/test-instance \
  -H "apikey: YOUR_TOKEN" \
  -d '{
    "webhook": {
      "enabled": true,
      "url": "https://abc123.ngrok.io/webhook"
    }
  }'
5

Test Events

Send a test message to your WhatsApp and watch webhooks arrive in real-time.

Webhook Testing Tools

  • Webhook.site - Inspect webhook payloads without code
  • RequestBin - Collect and debug webhooks
  • Postman - Mock webhook servers for testing

Next Steps

Instances

Learn how to create and manage instances

Authentication

Secure your API with authentication

Multi-Tenant

Build multi-tenant applications

Message Events

Explore message API endpoints

Build docs developers (and LLMs) love