Guide

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.

August 11, 2026By Retention Stack
Open in AI

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:

bash
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"]
  }'
javascript
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:

FieldRequiredWhat it does
webhookUrlYesThe HTTPS URL that receives a POST for each event
eventsNoArray of event names to subscribe to. Defaults to ["message"] if omitted
hmacKeyNoSecret key. When set, every request includes an X-Webhook-Hmac signature header
retriesNo{ policy: "exponential", delaySeconds: 2, attempts: 5 }
customHeadersNoArray 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:

EventFires when
session.statusSession state changes: STARTING, SCAN_QR_CODE, AUTHENTICATED, STOPPED, FAILED
messageA new incoming message arrives
message.anyAny message — incoming or outgoing
message.ackA message you sent is delivered or read
message.reactionSomeone reacts to a message
message.revokedA message was deleted after sending
message.editedA message was edited after sending
group.v2.join / group.v2.leaveSession joins or leaves a group
group.v2.participantsA participant is added or removed from a group
presence.updateA contact's online/typing status changes
call.receivedAn incoming call arrives
poll.voteA 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:

json
{
  "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:

json
{
  "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.

javascript
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:

ackackNameMeaning
-1ERRORFailed to send
0PENDINGQueued, not yet sent
1SERVERReached WhatsApp's servers (one grey tick)
2DEVICEDelivered to the recipient's device (two grey ticks)
3READRead by the recipient (two blue ticks)
4PLAYEDVoice 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 message event will crash or silently misbehave the first time a session.status or message.ack event lands on the same endpoint.

Key Takeaways

  1. 1.One POST registers a webhookwebhookUrl and events, no Meta App Dashboard or verification handshake required.
  2. 2.29 event types are available, but most integrations need three or four of them — message, message.ack, session.status, and maybe message.reaction.
  3. 3.A 200 on send means accepted, not deliveredmessage.ack with ackName: "READ" or "DEVICE" is the real confirmation.
  4. 4.hmacKey and X-Webhook-Hmac verify the request came from your session, not from anyone who guessed your URL.
  5. 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:

External resources:


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