Back to Blog
WhatsApp APIWebhooksTutorialGuide

WhatsApp Webhook Setup Guide

A step-by-step guide to setting up WhatsApp webhooks. Learn how to configure webhook URLs, handle incoming payloads, verify signatures, implement error handling, and build reliable real-time event processing.

Vesper Team9 min read

Webhooks are the backbone of any real-time WhatsApp integration. Instead of constantly polling the API to check for new messages or status updates, webhooks push events to your server the moment they happen. This guide walks you through setting up, securing, and maintaining WhatsApp webhooks — from initial configuration to production-grade error handling.

What Are Webhooks?

A webhook is an HTTP callback — a POST request that a service sends to your server when a specific event occurs. In the context of WhatsApp APIs, webhooks notify your application about events like:

  • A new message was received
  • A message you sent was delivered
  • A message was read by the recipient
  • A session connected or disconnected
  • A group was updated

Without webhooks, you would need to poll the API every few seconds asking "any new messages?" — which is wasteful, slow, and unreliable. Webhooks make your integration event-driven and efficient.

Step 1: Create Your Webhook Endpoint

First, you need an HTTP endpoint on your server that can receive POST requests. Here is a basic Express.js example:

import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/whatsapp', (req, res) => {
  console.log('Webhook received:', JSON.stringify(req.body, null, 2));

  // Always respond quickly with 200
  res.status(200).json({ status: 'ok' });
});

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

The critical rule: always respond with a 200 status code quickly. Process the event asynchronously if needed. If your endpoint takes too long to respond or returns an error, the provider may retry the delivery or eventually disable your webhook.

Step 2: Make Your Endpoint Publicly Accessible

During development, your local server is not accessible from the internet. You have several options:

Option A: ngrok (Development)

# Install ngrok and start a tunnel
ngrok http 3000

# You will get a URL like:
# https://abc123.ngrok.io
# Your webhook URL becomes:
# https://abc123.ngrok.io/webhooks/whatsapp

Option B: Deploy to a VPS (Production)

For production, deploy your webhook server to a cloud provider (Hetzner, AWS, DigitalOcean, etc.) behind a reverse proxy like Nginx with SSL:

# Nginx configuration
server {
    listen 443 ssl;
    server_name webhooks.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/webhooks.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/webhooks.yourdomain.com/privkey.pem;

    location /webhooks/whatsapp {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Step 3: Configure the Webhook in Your Dashboard

With Vesper, webhook configuration is done through the dashboard or API. Navigate to your app settings and enter your webhook URL. You can also configure which events you want to receive.

// Alternatively, configure via API
const response = await fetch('https://api.vespergateway.com/v1/apps/settings', {
  method: 'PUT',
  headers: {
    'Authorization': 'Bearer vsp_your_api_key',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    webhookUrl: 'https://webhooks.yourdomain.com/webhooks/whatsapp',
    webhookEvents: [
      'message.received',
      'message.sent',
      'message.delivered',
      'message.read',
      'session.connected',
      'session.disconnected',
    ],
  }),
});

Step 4: Understanding Webhook Payloads

Each webhook event follows a consistent JSON structure. Here are the most common event types and their payloads:

Message Received

{
  "type": "message.received",
  "timestamp": "2026-03-15T10:30:00Z",
  "data": {
    "messageId": "3EB0A1B2C3D4E5F6",
    "sessionId": "my-session",
    "from": "905551234567@s.whatsapp.net",
    "fromName": "John Doe",
    "to": "905559876543@s.whatsapp.net",
    "text": "Hello, I need help with my order",
    "timestamp": 1710495000,
    "isGroup": false,
    "hasMedia": false
  }
}

Message Delivered

{
  "type": "message.delivered",
  "timestamp": "2026-03-15T10:30:05Z",
  "data": {
    "messageId": "3EB0F6E5D4C3B2A1",
    "sessionId": "my-session",
    "to": "905551234567@s.whatsapp.net",
    "status": "delivered",
    "timestamp": 1710495005
  }
}

Message with Media

{
  "type": "message.received",
  "timestamp": "2026-03-15T10:31:00Z",
  "data": {
    "messageId": "3EB0C1D2E3F4A5B6",
    "sessionId": "my-session",
    "from": "905551234567@s.whatsapp.net",
    "fromName": "John Doe",
    "text": "",
    "caption": "Check this out",
    "hasMedia": true,
    "mediaType": "image",
    "mediaUrl": "https://api.vespergateway.com/v1/media/abc123"
  }
}

Session Status Change

{
  "type": "session.disconnected",
  "timestamp": "2026-03-15T10:32:00Z",
  "data": {
    "sessionId": "my-session",
    "reason": "phone_offline",
    "lastSeen": "2026-03-15T10:31:55Z"
  }
}

Step 5: Verifying Webhook Signatures

Security is critical. Without signature verification, anyone who discovers your webhook URL could send fake events to your server. Vesper signs every webhook with HMAC-SHA256 using your webhook secret.

import crypto from 'crypto';

function verifySignature(rawBody, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(signature, 'hex')
  );
}

// Middleware for webhook verification
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-vesper-signature'];
  const rawBody = req.body.toString();

  if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const event = JSON.parse(rawBody);
  processEvent(event);
  res.status(200).json({ status: 'ok' });
});

Step 6: Processing Events Asynchronously

Your webhook endpoint should respond as fast as possible. Heavy processing — database writes, API calls to external services, sending reply messages — should happen asynchronously. A message queue is the gold standard for this:

import { Queue } from 'bullmq';
import Redis from 'ioredis';

const connection = new Redis(process.env.REDIS_URL);
const webhookQueue = new Queue('whatsapp-webhooks', { connection });

app.post('/webhooks/whatsapp', async (req, res) => {
  // Verify signature first (see above)

  // Add to queue for async processing
  await webhookQueue.add(req.body.type, req.body, {
    removeOnComplete: 1000,
    removeOnFail: 5000,
    attempts: 3,
    backoff: {
      type: 'exponential',
      delay: 2000,
    },
  });

  res.status(200).json({ status: 'queued' });
});

Then process the queue with a worker:

import { Worker } from 'bullmq';

const worker = new Worker('whatsapp-webhooks', async (job) => {
  const event = job.data;

  switch (event.type) {
    case 'message.received':
      await saveMessageToDatabase(event.data);
      await sendAutoReply(event.data);
      break;

    case 'message.delivered':
      await updateMessageStatus(event.data.messageId, 'delivered');
      break;

    case 'message.read':
      await updateMessageStatus(event.data.messageId, 'read');
      break;

    case 'session.disconnected':
      await alertTeam(event.data.sessionId, event.data.reason);
      break;
  }
}, { connection });

Step 7: Error Handling and Retry Logic

Things will go wrong. Your database might be down, an external API might timeout, or your code might throw an unexpected error. Plan for these scenarios:

Idempotency

Webhook deliveries can be retried if your server did not respond with 200 quickly enough. This means you might receive the same event twice. Use the messageId or event timestamp as an idempotency key:

const processedEvents = new Set();

async function processEvent(event) {
  const eventKey = `${event.type}:${event.data.messageId || event.timestamp}`;

  if (processedEvents.has(eventKey)) {
    console.log('Duplicate event, skipping:', eventKey);
    return;
  }

  processedEvents.add(eventKey);

  // For production, use Redis instead of an in-memory Set
  // await redis.set(`webhook:processed:${eventKey}`, '1', 'EX', 86400);

  // Process the event...
}

Dead Letter Queue

Events that fail processing after multiple retries should go to a dead letter queue for manual inspection:

const deadLetterQueue = new Queue('whatsapp-webhooks-dlq', { connection });

worker.on('failed', async (job, err) => {
  if (job.attemptsMade >= job.opts.attempts) {
    console.error(`Event permanently failed after ${job.attemptsMade} attempts:`, err);
    await deadLetterQueue.add('failed-event', {
      originalEvent: job.data,
      error: err.message,
      failedAt: new Date().toISOString(),
    });
  }
});

Step 8: Monitoring and Alerting

In production, you need visibility into your webhook processing pipeline. Track these metrics:

  • Events received per minute: Baseline for detecting anomalies
  • Processing latency: How long it takes to handle each event
  • Error rate: Percentage of events that fail processing
  • Queue depth: How many events are waiting to be processed
  • Dead letter queue size: Events that need manual attention
// Simple monitoring with logging
app.post('/webhooks/whatsapp', (req, res) => {
  const startTime = Date.now();

  // Process event...

  const duration = Date.now() - startTime;
  console.log(`Webhook processed in ${duration}ms: ${req.body.type}`);

  if (duration > 1000) {
    console.warn(`Slow webhook processing: ${duration}ms for ${req.body.type}`);
  }

  res.status(200).json({ status: 'ok' });
});

Common Pitfalls

Avoid these mistakes that developers frequently make with webhook integrations:

  1. Synchronous processing: Never do heavy work in the webhook handler. Respond with 200 immediately and process asynchronously.
  2. No signature verification: Always verify HMAC signatures. Skipping this is a security vulnerability.
  3. No idempotency: Events can be delivered more than once. Handle duplicates gracefully.
  4. Hardcoded webhook URLs: Use environment variables so you can switch between dev/staging/prod easily.
  5. No error alerting: If your webhook processing starts failing, you need to know immediately — not when customers complain.
  6. Ignoring session events: Monitor session.disconnected events. If a session drops and you do not reconnect, you will silently stop receiving messages.

Testing Your Webhooks

Before going live, test your webhook implementation thoroughly:

# Send a test webhook event using curl
curl -X POST https://your-server.com/webhooks/whatsapp \
  -H "Content-Type: application/json" \
  -H "x-vesper-signature: test_signature" \
  -d '{
    "type": "message.received",
    "timestamp": "2026-03-15T10:30:00Z",
    "data": {
      "messageId": "TEST123",
      "sessionId": "test-session",
      "from": "905551234567@s.whatsapp.net",
      "fromName": "Test User",
      "text": "Test message"
    }
  }'

Vesper also provides a "Test Webhook" button in the dashboard that sends a sample event to your configured URL, letting you verify the connection without sending real WhatsApp messages.

Conclusion

A well-implemented webhook system is the foundation of a reliable WhatsApp integration. By following the steps in this guide — securing your endpoint with HMAC verification, processing events asynchronously with a message queue, handling duplicates with idempotency, and monitoring your pipeline — you will build a production-grade system that handles thousands of events without breaking a sweat.

If you are getting started with WhatsApp webhooks, Vesper makes the process straightforward with HMAC-signed deliveries, a clean event format, and a dashboard that lets you test and monitor your webhook in real time.

Ready to get started?

Start integrating WhatsApp into your application today with Vesper. Free 7-day trial, no credit card required.

Start Free Trial