Skip to content

Webhooks

Instead of repeatedly calling the read API to check for new messages, register a callback URL once. Whatbot then sends a signed HTTP POST to that URL every time something happens — a customer replies, or one of your messages is delivered/read. Your server just needs to expose an endpoint that accepts POST.

It’s the same idea as Stripe, GitHub, or Meta webhooks. (Your live inbox UI uses a WebSocket; external integrations use webhooks — the server-to-server equivalent.)

cURL
curl -X POST https://api.whatbot.in/public/v1/webhooks \
-H "X-API-Key: wa_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/whatbot/webhook",
"events": ["message.received", "message.status"]
}'

Response — the signing secret is returned once. Store it; you need it to verify every incoming event.

{
"id": "b6e92e7e-...",
"url": "https://your-app.com/whatbot/webhook",
"events": ["message.received", "message.status"],
"secret": "0DghFF2Hn55hFM8yIQA9BdK_-HA7wZupj-YnlXVBkDQ"
}

events is optional — it defaults to both. You can register multiple URLs.

Every delivery is a POST with this envelope:

{
"event": "message.received",
"data": { ... },
"timestamp": "2026-07-10T05:11:30Z"
}

message.received — a customer sent you a message

Section titled “message.received — a customer sent you a message”
{
"event": "message.received",
"data": {
"conversation_id": "c9e6f0f7-...",
"phone": "919876543210",
"message": { "id": "wamid...", "type": "text", "text": "Hi, is this in stock?", "timestamp": "..." }
}
}

message.status — delivery receipt for a message you sent

Section titled “message.status — delivery receipt for a message you sent”
{
"event": "message.status",
"data": {
"message_id": "wamid...",
"wa_message_id": "wamid...",
"status": "delivered",
"recipient": "919876543210"
}
}

status is one of sent, delivered, read, failed.

Every request includes:

  • X-Whatbot-Event: message.received
  • X-Whatbot-Signature: sha256=<hex> where <hex> is HMAC-SHA256(secret, raw_body)

Compute the HMAC over the raw request body and compare — reject anything that doesn’t match, so nobody can forge events.

Node / Express
import crypto from "crypto";
app.post("/whatbot/webhook", express.raw({ type: "*/*" }), (req, res) => {
const sig = req.header("X-Whatbot-Signature") || "";
const expected = "sha256=" + crypto
.createHmac("sha256", process.env.WHATBOT_WEBHOOK_SECRET)
.update(req.body) // raw bytes, not parsed JSON
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(401);
}
const { event, data } = JSON.parse(req.body);
// handle event...
res.sendStatus(200); // 2xx = acknowledged
});
Python / Flask
import hmac, hashlib
from flask import request, abort
@app.post("/whatbot/webhook")
def webhook():
raw = request.get_data()
expected = "sha256=" + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
if not hmac.compare_digest(request.headers.get("X-Whatbot-Signature", ""), expected):
abort(401)
payload = request.get_json()
# handle payload["event"] / payload["data"]
return "", 200
  • Respond 2xx to acknowledge. A non-2xx or timeout is retried a couple of times with backoff.
  • Deliveries are best-effort and never block message processing on Whatbot’s side, so make your handler fast (queue the work, then return 200).
  • A retried event may arrive more than once — make your handler idempotent by deduping on wa_message_id.
List
curl https://api.whatbot.in/public/v1/webhooks -H "X-API-Key: wa_live_xxx"
Delete
curl -X DELETE https://api.whatbot.in/public/v1/webhooks/{id} -H "X-API-Key: wa_live_xxx"

The secret is never returned by the list endpoint — only at creation time.