Webhooks
Why webhooks
Section titled “Why 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.)
Register a webhook
Section titled “Register a webhook”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.
Events
Section titled “Events”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.
Verify the signature
Section titled “Verify the signature”Every request includes:
X-Whatbot-Event: message.receivedX-Whatbot-Signature: sha256=<hex>where<hex>isHMAC-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.
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});import hmac, hashlibfrom 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 "", 200Delivery & retries
Section titled “Delivery & retries”- Respond
2xxto 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.
Manage webhooks
Section titled “Manage webhooks”curl https://api.whatbot.in/public/v1/webhooks -H "X-API-Key: wa_live_xxx"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.