cd /blog
GET/blog/whatsapp-webhook-setup-real-time-processing—200OK
tutorialwebhooksnodejspython

WhatsApp Webhook Setup: Real-Time Message Processing

March 19, 2026|9 min read

Real-time message processing is the backbone of any interactive WhatsApp integration. Whether you're building a chatbot, syncing messages to a CRM, or tracking delivery receipts, you need to receive events as they happen. WSAPI offers two real-time delivery mechanisms: HTTP webhooks and Server-Sent Events (SSE). This guide covers both.

Webhooks vs SSE: Which to Choose

FeatureWebhooksSSE
DirectionWSAPI pushes to youYou connect to WSAPI
ConnectionPer-event HTTP POSTPersistent stream
ReliabilityAuto-retry on failureReconnect on drop
FirewallNeeds public endpointOutbound only
Best forServers, productionLocal dev, debugging

Use webhooks for production. Use SSE for local development or when you can't expose a public endpoint. You can use both simultaneously.

Setting Up Webhooks

Step 1: Configure Your Webhook URL

Point your WSAPI instance to your webhook endpoint via the dashboard or API:

PUTconfigure_webhook.sh
curl -X PUT https://api.wsapi.chat/account/instances/{instanceId}/settings \
  -H "x-api-key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://your-app.com/webhooks/whatsapp",
    "eventSigningSecret": "a-long-random-string",
    "eventFilters": ["message", "message_read", "user_presence", "group"]
  }'

Step 2: Build a Webhook Receiver (Node.js)

webhook.js
import express from 'express';
import crypto from 'crypto';

const app = express();
const SIGNING_SECRET = process.env.WSAPI_SIGNING_SECRET;

// Fail closed, at startup: with no secret configured nothing is signed and
// nothing can be verified, and a receiver that skips the check looks exactly
// like one that passes it. Better to refuse to boot than to accept anything.
if (!SIGNING_SECRET) {
  throw new Error('WSAPI_SIGNING_SECRET is not set: configure a signing secret on your instance first');
}

// The signature covers the bytes WSAPI sent, so the body is read raw and
// parsed only after the check. express.json() would re-serialise it and
// every signature would fail.
app.post('/webhooks/whatsapp', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifySignature(req.body, req.header('X-Webhook-Signature'))) {
    return res.sendStatus(401);
  }

  let payload;
  try {
    payload = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }

  const { eventType, eventData } = payload;

  switch (eventType) {
    case 'message': {
      const direction = eventData.sender.isMe ? 'SENT' : 'RECV';
      console.log(`[${direction}] ${eventData.sender.id}: ${eventData.text ?? '[' + eventData.type + ']'}`);
      break;
    }
    case 'message_read':
      console.log(`[RECEIPT] ${eventData.receiptType} in ${eventData.chatId}`);
      break;
    case 'user_presence':
      console.log(`[PRESENCE] ${eventData.id} is ${eventData.status}`);
      break;
    case 'group':
      console.log(`[GROUP] ${eventData.id} changed`);
      break;
  }

  res.sendStatus(200);
});

function verifySignature(rawBody, header) {
  if (!header) return false;

  const expected =
    'sha256=' + crypto.createHmac('sha256', SIGNING_SECRET).update(rawBody).digest('hex');

  // timingSafeEqual throws when the two buffers differ in length, which is
  // what a forged signature usually looks like, so compare lengths first.
  if (expected.length !== header.length) return false;
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}

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

Step 3: Build a Webhook Receiver (Python)

webhook.py
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
# Fail closed at startup: no secret means nothing is verified.
SIGNING_SECRET = os.environ["WSAPI_SIGNING_SECRET"]

@app.post("/webhooks/whatsapp")
async def webhook(request: Request):
    raw = await request.body()          # the signature covers these bytes
    signature = request.headers.get("X-Webhook-Signature", "")

    expected = "sha256=" + hmac.new(
        SIGNING_SECRET.encode(), raw, hashlib.sha256
    ).hexdigest()
    if not hmac.compare_digest(expected, signature):
        raise HTTPException(status_code=401, detail="Invalid signature")

    payload = json.loads(raw)
    event_type = payload["eventType"]
    data = payload["eventData"]

    if event_type == "message":
        direction = "SENT" if data["sender"]["isMe"] else "RECV"
        print(f"[{direction}] {data['sender']['id']}: {data.get('text', '[' + data['type'] + ']')}")

    elif event_type == "message_read":
        print(f"[RECEIPT] {data['receiptType']} in {data['chatId']}")

    return {"status": "ok"}

Event Types Deep Dive

WSAPI delivers events in four categories. Subscribe only to the events you need to reduce noise and bandwidth:

Message Events

  • message: a new message, incoming or outgoing (text, media, location, contact, reaction)
  • message_delete: a message was deleted by its sender
  • message_star: a message was starred or unstarred
  • message_history_sync: messages arriving from an on-demand history sync

Delivery Events

  • message_read: a receipt, with receiptType one of delivered, read, played, readSelf and a few more. It is a string, not a numeric code

Presence Events

  • user_presence: a contact went available or unavailable
  • chat_presence: someone is typing, recording or paused in a chat

Group Events

  • group: one event for every group change. The payload carries what changed: join and leave arrays, promote and demote, name, description, locked, announce

Session Events

  • logged_in, logged_out, login_error, initial_sync_finished: these four are always delivered and cannot be filtered out

SSE: Server-Sent Events

SSE is perfect for local development or environments where you can't expose a public webhook URL. Connect to the WSAPI event stream and receive events over a persistent HTTP connection:

This runs on your server, never in the browser. The stream authenticates with your API key, and any key placed in browser code is handed to every visitor whether it sits in the URL or in a header. Both clients below are server side for that reason.

sse_client.js
// npm install eventsource
import { EventSource } from 'eventsource';

// The stream authenticates with the same two headers as the REST API. The
// browser's built-in EventSource cannot send headers, so this runs in Node
// and injects them through a custom fetch.
const stream = new EventSource('https://api.wsapi.chat/events/stream', {
  fetch: (url, init) =>
    fetch(url, {
      ...init,
      headers: {
        ...init.headers,
        'x-api-key': process.env.WSAPI_KEY,
        'x-instance-id': process.env.INSTANCE_ID,
      },
    }),
});

stream.onmessage = (e) => {
  const { eventType, eventData } = JSON.parse(e.data);

  if (eventType === 'message') {
    console.log('New message:', eventData.sender.id, eventData.text);
  }
};

stream.onerror = () => {
  console.log('Connection lost, reconnecting...');
};
sse_client.py
import json
import os
import requests

with requests.get(
    "https://api.wsapi.chat/events/stream",
    headers={
        "x-api-key": os.environ["WSAPI_KEY"],
        "x-instance-id": os.environ["INSTANCE_ID"],
    },
    stream=True,
) as response:
    response.raise_for_status()

    for line in response.iter_lines():
        if not line.startswith(b"data: "):
            continue

        payload = json.loads(line[len(b"data: "):])
        print(payload["eventType"], payload["eventData"])

Production Best Practices

  1. Always verify signatures: check the X-Webhook-Signature header, which carries sha256= followed by the HMAC-SHA256 of the raw body, to validate that a payload came from WSAPI.
  2. Respond quickly: Return a 200 status within 5 seconds. Do heavy processing asynchronously (queue the event, process later).
  3. Handle retries idempotently: WSAPI retries failed deliveries. Use the event ID to deduplicate if your handler isn't idempotent.
  4. Use HTTPS: Always use HTTPS for your webhook endpoint. WSAPI will not deliver to plain HTTP in production.
  5. Monitor delivery: Track webhook delivery success rates in your logging. The WSAPI dashboard also shows delivery stats.
  6. Filter events: Only subscribe to events you actually process. Fewer events = less load, lower latency.

Next Steps