Receive Messages via Webhook

Overview

When a WhatsApp user sends a message to one of your connected numbers, Muchaw DEV routes that message to your webhook URL. This guide walks through setting up a webhook server that securely receives and processes these events.

Step 1 — Create a webhook

$curl -X POST https://dev.muchau.com.br/api/webhooks \
> -H "Authorization: Bearer $MUCHAW_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://your-server.com/webhooks/whatsapp",
> "events": [
> "message.received",
> "message.delivered",
> "message.read",
> "message.failed"
> ]
> }'

Save the secret from the response — you’ll need it to verify incoming requests.

$export MUCHAW_WEBHOOK_SECRET=whsec_...

Step 2 — Build the webhook handler

1import express from "express";
2import crypto from "node:crypto";
3
4const app = express();
5
6function verifySignature(rawBody, signature, secret) {
7 if (!signature?.startsWith("sha256=")) return false;
8 const expected = crypto
9 .createHmac("sha256", secret)
10 .update(rawBody)
11 .digest("hex");
12 return crypto.timingSafeEqual(
13 Buffer.from(signature.slice(7)),
14 Buffer.from(expected)
15 );
16}
17
18// Use raw body parser to preserve the original bytes for signature verification
19app.post(
20 "/webhooks/whatsapp",
21 express.raw({ type: "application/json" }),
22 (req, res) => {
23 // 1. Verify signature
24 const signature = req.headers["x-muchaw-signature"];
25 if (!verifySignature(req.body, signature, process.env.MUCHAW_WEBHOOK_SECRET)) {
26 return res.status(401).json({ error: "Invalid signature" });
27 }
28
29 // 2. Respond immediately (before processing)
30 res.sendStatus(200);
31
32 // 3. Parse and process
33 const event = JSON.parse(req.body.toString());
34 handleEvent(event);
35 }
36);
37
38function handleEvent(event) {
39 switch (event.event) {
40 case "message.received":
41 console.log("New message from", event.data.fromNumber, ":", event.data.body);
42 break;
43 case "message.delivered":
44 console.log("Message", event.data.id, "delivered");
45 break;
46 case "message.read":
47 console.log("Message", event.data.id, "read");
48 break;
49 case "message.failed":
50 console.error("Message", event.data.id, "failed to deliver");
51 break;
52 }
53}
54
55app.listen(3001, () => console.log("Webhook server on port 3001"));

Always respond with 200 before processing. Muchaw DEV waits up to 10 seconds for a response. If your processing takes longer, respond immediately and handle the event asynchronously.

Step 3 — Expose your server publicly

For local development, use a tunnel like ngrok:

$ngrok http 3001
$# → Forwarding: https://abc123.ngrok.io → localhost:3001

Update your webhook URL to the ngrok URL.

Step 4 — Test the webhook

$curl -X POST "https://dev.muchau.com.br/api/webhooks/<webhook_id>/test" \
> -H "Authorization: Bearer $MUCHAW_API_KEY"

Response:

1{
2 "success": true,
3 "statusCode": 200,
4 "responseTimeMs": 87,
5 "message": "Test webhook delivered successfully"
6}

Webhook event structure

All events follow the same envelope:

1{
2 "event": "message.received",
3 "timestamp": "2024-01-15T10:05:00.000Z",
4 "data": { ... }
5}

The data shape depends on the event type. For message.* events, it’s a Message object. For number.* events, it’s a Number object.

Handling retries

Muchaw DEV retries events up to 3 times with exponential backoff. Your handler should be idempotent — processing the same event twice must not cause side effects (duplicate records, duplicate sends, etc.).

Use event.data.id as a deduplication key:

1const processed = new Set();
2function handleEvent(event) {
3 const key = `${event.event}:${event.data.id}`;
4 if (processed.has(key)) return;
5 processed.add(key);
6 // process...
7}

In production, use Redis or your database for deduplication.