Documentation

Webhook Setup

Receive real-time WhatsApp delivery receipts and inbound messages in your application

Webhook Setup

Webhooks let Chatbase notify your server instantly when messages are delivered, read, or when customers reply. This guide shows you how to set one up with Node.js / Express.


How It Works

Customer sends message
        ↓
  WhatsApp / Meta
        ↓
   Chatbase server
        ↓
  POST to your webhook
        ↓
  Your application processes event

Step 1 — Build Your Webhook Endpoint

Create a public HTTPS endpoint that accepts POST requests:

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

// Use raw body parser for signature verification
app.use(express.json());

app.post('/webhook/whatsapp', (req, res) => {
  // Always respond 200 immediately
  res.sendStatus(200);

  const { event, ...payload } = req.body;
  processEvent(event, payload);
});

function processEvent(event, payload) {
  switch (event) {
    case 'message':
      console.log(`Inbound from ${payload.from}: ${payload.text?.body}`);
      break;

    case 'message_status':
      console.log(`Message ${payload.messageId} is now: ${payload.status}`);
      break;

    case 'template_status':
      console.log(`Template "${payload.templateName}" status: ${payload.status}`);
      break;
  }
}

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

Step 2 — Make It Public

During development, use a tunneling tool to expose your local server:

# Using ngrok
npx ngrok http 3000

# Output:
# Forwarding  https://abc123.ngrok.io -> http://localhost:3000

Your webhook URL will be: https://abc123.ngrok.io/webhook/whatsapp

For production, deploy to a server with a real HTTPS domain.


Step 3 — Register in the Dashboard

  1. Go to CRM dashboard → IntegrationsWhatsApp
  2. Find the Webhook URL field
  3. Enter your endpoint URL
  4. Save — Chatbase sends a verification challenge and your endpoint must respond correctly

Step 4 — Handle the Verification Challenge

When you register a webhook URL, Chatbase sends a GET request to verify ownership:

app.get('/webhook/whatsapp', (req, res) => {
  const mode      = req.query['hub.mode'];
  const token     = req.query['hub.verify_token'];
  const challenge = req.query['hub.challenge'];

  if (mode === 'subscribe' && token === process.env.WEBHOOK_VERIFY_TOKEN) {
    res.status(200).send(challenge);  // Echo back the challenge
  } else {
    res.sendStatus(403);
  }
});

Set WEBHOOK_VERIFY_TOKEN in your .env to any secret string. Enter the same value in the dashboard when registering your webhook.


Step 5 — Verify Request Signatures (Production)

In production, verify that incoming webhook requests genuinely come from Chatbase:

const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature ?? ''),
    Buffer.from(expected)
  );
}

// Use raw body middleware to get the unparsed body
app.post('/webhook/whatsapp',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const sig = req.headers['x-chatbase-signature'];

    if (!verifySignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
      return res.sendStatus(401);
    }

    res.sendStatus(200);
    const payload = JSON.parse(req.body);
    processEvent(payload.event, payload);
  }
);

Full Event Payloads

Inbound Text Message

{
  "event": "message",
  "from": "919876543210",
  "name": "Rahul Sharma",
  "messageId": "wamid.HBgLOTE5ODc2NTQzMjEwFQ...",
  "type": "text",
  "text": { "body": "Hello, I need help with my order" },
  "timestamp": "2024-06-10T09:30:00.000Z"
}

Inbound Image

{
  "event": "message",
  "from": "919876543210",
  "type": "image",
  "image": {
    "id": "media-id-xxx",
    "mimeType": "image/jpeg",
    "caption": "Here is my receipt"
  },
  "timestamp": "2024-06-10T09:31:00.000Z"
}

Delivery Status Update

{
  "event": "message_status",
  "messageId": "wamid.HBgLOTE5ODc2NTQzMjEwFQ...",
  "to": "919876543210",
  "status": "read",
  "timestamp": "2024-06-10T09:30:10.000Z"
}

Status progression: sentdeliveredread (or failed)


Auto-Reply Example

Reply automatically when a customer messages you:

async function processEvent(event, payload) {
  if (event !== 'message') return;

  const { from, text } = payload;
  const body = text?.body?.toLowerCase() ?? '';

  let reply = 'Thanks for your message! Our team will respond shortly.';

  if (body.includes('price') || body.includes('cost')) {
    reply = 'Check our pricing at https://chatbase.in/pricing';
  } else if (body.includes('help')) {
    reply = 'I can help! Reply with: ORDER, TRACK, RETURN, or SUPPORT';
  }

  await api.post('/messages/send', {
    to: from,
    type: 'text',
    text: { body: reply },
  });
}

Retry Policy

If your server is unavailable or returns a non-200 status:

AttemptDelay
1st retry5 seconds
2nd retry30 seconds
3rd retry5 minutes
4th retry30 minutes

Design your webhook handler to be idempotent — the same event may be delivered more than once.


Checklist

  • Endpoint responds 200 OK immediately (process async)
  • Verification challenge handler on GET same path
  • Signature verification in production
  • Idempotent event handling (deduplicate by messageId)
  • Retry logic for downstream calls
  • Logging for debugging

Next Steps