WhatsApp API Webhooks: How to Receive Messages Without Polling
How WhatsApp API webhooks work: registering an endpoint in one API call, the 29 event types you can subscribe to, verifying payloads with HMAC, and confirming real delivery with message.ack.
Meta Description: How WhatsApp API webhooks work: registering an endpoint in one API call, the 29 event types you can subscribe to, verifying payloads with HMAC, and confirming real delivery with message.ack.
Introduction
Polling an API for new messages means asking "anything new yet?" every few seconds, forever, on every session you run. It works. It also wastes requests, adds latency between a message arriving and your bot seeing it, and burns through your plan's monthly quota on empty responses.
A webhook flips that around. You register one HTTPS URL, and the API pushes an HTTP POST to it the moment something happens: a new message, a delivery confirmation, a session going offline. Your server sits idle until there's real work to do.
This is the same event-driven pattern Stripe uses for payments and GitHub uses for pushes, and if you've set up a webhook for either of those before, most of what follows will look familiar. What's different here is the setup itself: no Meta App Dashboard, no App Review, no hub.challenge verification handshake. You send one POST request with a URL and a list of event names, and you're subscribed.
Register a Webhook in One API Call
Every session on this API can have a webhook attached to it. Registering one is a single POST to /v1/sessions/{session}/webhooks:
curl -X POST "https://whatsapp-messaging-bot.p.rapidapi.com/v1/sessions/{session}/webhooks" \
-H "x-rapidapi-key: YOUR_API_KEY" \
-H "x-rapidapi-host: whatsapp-messaging-bot.p.rapidapi.com" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://your-server.com/webhook",
"events": ["message", "session.status"]
}'const axios = require('axios');
await axios.post(
'https://whatsapp-messaging-bot.p.rapidapi.com/v1/sessions/{session}/webhooks',
{
webhookUrl: 'https://your-server.com/webhook',
events: ['message', 'session.status'],
// optional: hmacKey, retries, customHeaders
},
{
headers: {
'x-rapidapi-key': process.env.RAPIDAPI_KEY,
'x-rapidapi-host': 'whatsapp-messaging-bot.p.rapidapi.com',
'Content-Type': 'application/json',
},
}
);Removing it later is the same endpoint with DELETE.
Request body fields:
| Field | Required | What it does |
|---|---|---|
webhookUrl | Yes | The HTTPS URL that receives a POST for each event |
events | No | Array of event names to subscribe to. Defaults to ["message"] if omitted |
hmacKey | No | Secret key. When set, every request includes an X-Webhook-Hmac signature header |
retries | No | { policy: "exponential", delaySeconds: 2, attempts: 5 } |
customHeaders | No | Array of { name, value } headers added to every request, e.g. for a shared auth token |
Skip events entirely and you'll only get new incoming messages. Most integrations need at least message and session.status — the second tells you if the session drops offline before your users notice a broken bot.
Which Events You Can Subscribe To
The API supports 29 event types. The ones you'll actually use, most of the time:
| Event | Fires when |
|---|---|
session.status | Session state changes: STARTING, SCAN_QR_CODE, AUTHENTICATED, STOPPED, FAILED |
message | A new incoming message arrives |
message.any | Any message — incoming or outgoing |
message.ack | A message you sent is delivered or read |
message.reaction | Someone reacts to a message |
message.revoked | A message was deleted after sending |
message.edited | A message was edited after sending |
group.v2.join / group.v2.leave | Session joins or leaves a group |
group.v2.participants | A participant is added or removed from a group |
presence.update | A contact's online/typing status changes |
call.received | An incoming call arrives |
poll.vote | A vote is cast on a poll |
Subscribe only to what you use. A support bot needs message and message.ack; a bulk-send job mostly cares about message.ack for delivery confirmation; a dashboard tracking session health wants session.status and not much else. Extra events you never read are just extra load on your endpoint.
What Your Server Receives
Every event, regardless of type, arrives with the same envelope: event, session, and a payload whose shape depends on which event fired.
A new incoming message:
{
"event": "message",
"session": "my-session",
"payload": {
"id": "false_11111111111@c.us_AAABBBCCC",
"timestamp": 1667561485,
"from": "11111111111@c.us",
"fromMe": false,
"body": "Hello!",
"hasMedia": false,
"ack": 1,
"ackName": "SERVER"
}
}A delivery acknowledgement for a message you sent:
{
"event": "message.ack",
"session": "my-session",
"payload": {
"id": "true_11111111111@c.us_AAABBBCCC",
"from": "11111111111@c.us",
"to": "22222222222@c.us",
"fromMe": true,
"ack": 3,
"ackName": "READ"
}
}Route on event first, then handle payload per type. Extra fields can appear depending on the underlying engine, but event, session, and payload.id are stable — build your handler against those, not against the full payload shape.
Securing Your Webhook Endpoint
Anyone who finds your webhook URL can POST fake events to it unless you verify where they came from. Set hmacKey when you register the webhook, and every request arrives with an X-Webhook-Hmac header computed from that secret and the request body.
const crypto = require('crypto');
function isValidWebhook(rawBody, signature, hmacKey) {
const expected = crypto
.createHmac('sha256', hmacKey)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-hmac'];
if (!isValidWebhook(req.body, signature, process.env.WEBHOOK_HMAC_KEY)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body);
// handle event.event / event.payload
res.sendStatus(200);
});Use crypto.timingSafeEqual, not ===, for the comparison — a plain string comparison leaks timing information an attacker can use to guess the signature byte by byte. GitHub's webhook signature validation guide covers this same attack and the fix in more depth if you want the full explanation.
Confirming Delivery: the message.ack Event
A 200 response from a send endpoint means "accepted for sending," not "delivered." A message to a number that isn't on WhatsApp can still return success at send time. message.ack is what tells you what actually happened:
| ack | ackName | Meaning |
|---|---|---|
| -1 | ERROR | Failed to send |
| 0 | PENDING | Queued, not yet sent |
| 1 | SERVER | Reached WhatsApp's servers (one grey tick) |
| 2 | DEVICE | Delivered to the recipient's device (two grey ticks) |
| 3 | READ | Read by the recipient (two blue ticks) |
| 4 | PLAYED | Voice or video note played |
The reliable pattern for anything that matters — an OTP, an order confirmation, a payment receipt — is check the number exists, send, then wait for message.ack before you consider the job done. Sending and immediately moving on because you got a 200 is how "the message never arrived" tickets happen.
Best Practices
Return 200 immediately, process after. Your endpoint should acknowledge the request and do the actual work (database writes, AI calls, forwarding to a CRM) on a queue afterward. A slow handler that takes several seconds to respond looks the same as a dead endpoint to a delivery system, and retries pile up. Stripe's own webhook guidance says the same thing for the same reason: return the 2xx before the complex logic, not after it.
Dedupe by message ID. Retries mean you can receive the same event more than once. Use payload.id as an idempotency key before you act on an event a second time.
Set retries instead of assuming delivery. The retries field ({ policy: "exponential", delaySeconds: 2, attempts: 5 }) controls what happens if your endpoint is briefly down. Configure it rather than finding out the hard way after a deploy causes a 30-second gap.
Verify the signature before you trust the body. Covered above — skip this and your webhook endpoint is an open door for anyone who finds the URL.
Subscribe to fewer events, not more. Every subscribed event is a request your server has to handle. message.any doubles your inbound traffic over message alone by including your own outgoing messages — only subscribe to it if you actually need to mirror outbound activity somewhere.
Common Mistakes
- •Still polling "just in case." If you registered a webhook, stop calling a messages-list endpoint on a timer. It defeats the purpose and burns your monthly request quota for nothing.
- •Trusting the request body without checking
X-Webhook-Hmac. An unverified webhook endpoint will accept a forged "message delivered" event as easily as a real one. - •Blocking the response on slow work. Sending a Slack notification, calling an LLM, or writing to three tables before responding turns a 50ms webhook into a multi-second one, and multi-second webhooks time out.
- •Only handling one event type. A handler that assumes every request is a
messageevent will crash or silently misbehave the first time asession.statusormessage.ackevent lands on the same endpoint.
Key Takeaways
- 1.One
POSTregisters a webhook —webhookUrlandevents, no Meta App Dashboard or verification handshake required. - 2.29 event types are available, but most integrations need three or four of them —
message,message.ack,session.status, and maybemessage.reaction. - 3.A
200on send means accepted, not delivered —message.ackwithackName: "READ"or"DEVICE"is the real confirmation. - 4.
hmacKeyandX-Webhook-Hmacverify the request came from your session, not from anyone who guessed your URL. - 5.Respond fast, process the payload after — a webhook handler that blocks on downstream work looks like a dead endpoint and triggers retries.
Related guides:
- •Build a WhatsApp Customer Support Bot — a full webhook-driven bot that reads
messageevents and replies automatically - •Build a WhatsApp Chatbot with Python — webhook handling in FastAPI instead of Node.js
- •WhatsApp CRM Integration — using webhook events to keep Salesforce, HubSpot, or Pipedrive in sync in real time
External resources:
- •Meta's WhatsApp Cloud API webhook setup guide — the official flow this API's one-call registration skips (App Dashboard config, permissions, verification challenge)
- •GitHub: validating webhook deliveries — the general HMAC-SHA256 signature verification technique used above, explained in more depth
- •Stripe: receive events in your webhook endpoint — the return-200-fast, process-async pattern referenced in Best Practices, from the company most webhook-consuming APIs modeled this on
Build the Webhook, Skip the App Review
No Meta App Dashboard, no permissions to request, no hub.challenge to answer correctly before your first event arrives. Register a URL, subscribe to the events you need, and start receiving messages.
Try Free on RapidAPI → Subscribe to Basic ($0) → Scan the QR code → Register your webhook and receive your first event.
Ready to Get Started?
Try the WhatsApp API free on RapidAPI with no credit card required.
Try Free on RapidAPI