WhatsApp Webhooks Guide: Events, Security and Retries
How WhatsApp webhooks work: message and status events, a reliable handler, Meta's signature check, retries, idempotency and recovery when events go missing.
By OmniChannels.ai Editorial Team · Reviewed by BroadNet Messaging Solutions Team
Integration
Short answer
A WhatsApp webhook is an HTTPS endpoint on your server that receives events: delivery statuses for messages you sent and messages customers send you. With OmniChannels.ai, statuses arrive as SUBMITTED, SENT, DELIVERED, READ, FAILED or REJECTED. Store the raw body, answer 2xx quickly, deduplicate by message ID and process asynchronously. Meta signs its own webhooks; our docs define no signature header, so secure your endpoint at the transport and application level.
On this page
- What are WhatsApp webhooks and why use them?
- Which events and statuses will I receive?
- How do I set up the endpoint?
- What does the payload look like?
- How do I write a reliable handler?
- How do I secure a WhatsApp webhook?
- What happens when delivery fails?
- How do I recover missing events?
- Worked example: a retailer's notification pipeline
- What are the common mistakes?
- How OmniChannels.ai helps
Key takeaways
- Webhooks push statuses and inbound messages to your HTTPS endpoint so you do not poll; WhatsApp statuses are normalised to SUBMITTED, SENT, DELIVERED, READ, FAILED and REJECTED.
- A reliable handler does three things in order: store the raw event, return 2xx, then process asynchronously and idempotently.
- Statuses can arrive out of order or twice, so rank them and never let a late SENT overwrite a stored DELIVERED.
- Meta signs webhooks sent to apps with an X-Hub-Signature-256 header, but the OmniChannels.ai API spec defines no webhook signature, so use HTTPS, an unguessable path and body validation.
- If events were missed, read WhatsApp status per batch from the bulk-insights endpoint instead of resending messages.
This guide is for engineers who need to receive WhatsApp events reliably: delivery statuses for the messages they send and inbound messages from customers. It explains how webhooks work, what to build in the handler, how security differs between Meta's webhooks and ours, and what to do when events go missing.
What are WhatsApp webhooks and why use them?
WhatsApp webhooks are HTTP callbacks. When a message changes state or a customer writes to you, the platform sends a POST with a JSON body to your endpoint. You do not have to poll, and you see events as they happen.
Two event families matter:
- Status events for messages you sent: queued, sent, delivered, read, failed.
- Inbound messages from customers, which open or extend the 24-hour customer service window. See our window guide.
A send call only tells you that messages were queued. The webhook is where you learn what happened to each recipient.
Which events and statuses will I receive?
On OmniChannels.ai every channel reports the same normalised statuses, so one handler can serve WhatsApp, SMS and the rest.
| Status | Meaning | Final? |
|---|---|---|
SUBMITTED |
Accepted by the platform and queued | No |
SENT |
Handed to the channel | No |
DELIVERED |
Confirmed on the recipient's device | Usually; READ may follow |
READ |
Opened by the recipient, where the channel reports reads | Yes |
FAILED |
Not delivered; the event carries a reason where available | Yes |
REJECTED |
Refused, for example blocked content or an unregistered sender | Yes |
Treat statuses as a progression that can arrive out of order, and expect new values over time. Do not fail on a status you do not recognise. See message delivery statuses explained.
How do I set up the endpoint?
Expose an HTTPS endpoint, give the URL to your provider, then test with your own number.
- Create an endpoint that accepts
POSTwith a JSON body, for examplehttps://api.example.com/webhooks/omnichannels. - Send the URL to our team or your account admin to configure it on your account. Resellers can have a separate webhook per sub-account.
- Send a test message to your own number and confirm the events arrive.
During development, a tunnel such as ngrok gives your local server a public HTTPS URL. If you also connect an app directly to Meta, Meta's endpoint verification is a separate step: a GET request carrying a verify token and a hub.challenge value, to which you reply 200 with the challenge, as described in Meta's webhook endpoint documentation.
What does the payload look like?
It varies by channel. The API spec does not define a single webhook schema, so log the raw body first and map the fields you need from real events on your account. The example shows the kind of information a delivery event carries and is illustrative only.
{
"messageId": "2603121407537573731",
"channel": "SMS",
"destination": "447700900123",
"status": "DELIVERED",
"timestamp": "2026-10-10T09:15:42Z",
"reason": null
}
Do not code against these field names without checking a real event. Ask your onboarding engineer for sample payloads per channel and keep them as test fixtures. For the broader documentation, see the webhooks reference.
How do I write a reliable handler?
Do three things in order: store the raw event, acknowledge with a 2xx, then process asynchronously. The handler below follows the pattern in our docs.
import express from "express";
const app = express();
// Capture the raw body so you can store it exactly as received.
app.post(
"/webhooks/omnichannels",
express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } }),
async (req, res) => {
try {
await db.webhookEvents.insert({ receivedAt: new Date(), body: req.rawBody });
} catch (err) {
return res.sendStatus(500); // storage failed: let the event be retried
}
res.sendStatus(200); // acknowledge fast
queue.publish("status-events", req.body); // do the real work elsewhere
},
);
// Worker: idempotent processing
async function processEvent(event) {
const id = event.messageId ?? event.message_id ?? event.id; // map per channel
const status = event.status;
const rank = { SUBMITTED: 1, SENT: 2, DELIVERED: 3, READ: 4, FAILED: 4, REJECTED: 4 };
const current = await db.messages.findOne({ providerId: id });
if (!current) return; // unknown message: log it and investigate
if ((rank[status] ?? 0) <= (rank[current.status] ?? 0)) return; // duplicate or stale
await db.messages.update({ providerId: id }, { status, updatedAt: new Date() });
}
from datetime import datetime, timezone
from flask import Flask, request
app = Flask(__name__)
RANK = {"SUBMITTED": 1, "SENT": 2, "DELIVERED": 3, "READ": 4, "FAILED": 4, "REJECTED": 4}
@app.post("/webhooks/omnichannels")
def omnichannels_webhook():
raw = request.get_data(as_text=True)
try:
db.webhook_events.insert({"received_at": datetime.now(timezone.utc), "body": raw})
except Exception:
return "", 500 # storage failed: let the event be retried
queue.publish("status-events", raw) # process asynchronously
return "", 200 # acknowledge fast
The db and queue objects are your own storage and job queue. The rank map is what makes processing idempotent: a duplicate or stale event is not an advance, so it is ignored.
Rules for a reliable handler
- Respond within a few seconds. Do no heavy work before you respond.
- Return
2xxonly after the event is stored. If storage fails, return5xx. - Deduplicate by message ID. The same status can arrive more than once.
- Never resend a message from the handler. Decide on retries in a separate job, after checking status.
- Keep the raw body for troubleshooting and for replaying events after a fix.
How do I secure a WhatsApp webhook?
It depends on who sends to your endpoint. Meta signs webhooks it sends to apps you register with Meta. The OmniChannels.ai API spec defines no webhook signature header, so do not wait for one.
| Meta to your own app | OmniChannels.ai to your endpoint | |
|---|---|---|
| Signature | X-Hub-Signature-256 header, per Meta's docs |
None defined in the API spec |
| Verification | HMAC-SHA256 of the raw payload with your app secret, compared with the header | Not applicable |
| Your controls | Check the signature on every POST |
HTTPS with a valid certificate, an unguessable path, strict body validation |
For Meta's scheme, Meta's documentation says to generate an HMAC-SHA256 hash from the JSON payload using your app secret and compare it with the value after sha256= in the X-Hub-Signature-256 header. HMAC itself is defined in RFC 2104. Compute it over the raw bytes, not a re-serialised object, and compare in constant time.
For our endpoint:
- Use HTTPS only, with a valid certificate.
- Use a long, unguessable path, and rotate it if it leaks.
- Validate the body shape and reject anything unexpected.
- Treat events as notifications. Before an irreversible action, such as a refund or fraud lock, confirm the state with a polling call.
What happens when delivery fails?
Retries depend on the sender. Meta's documentation says it retries a failed delivery with decreasing frequency for up to 7 days, which can produce duplicate notifications. That is a reason to be idempotent even if you rarely see duplicates. For events from our platform, confirm retry behaviour with your onboarding engineer instead of assuming it matches Meta's.
Either way, build for loss as well as duplication: your endpoint can be down during a deploy, and a firewall rule can drop requests silently.
How do I recover missing events?
Poll the batch, do not resend the message.
| Need | Call | Notes |
|---|---|---|
| Read WhatsApp status per recipient | GET /whatsapp/message/bulk-insights |
Filter a batch by status or destination; paginated with page and size |
| Re-push SMS delivery receipts | POST /sms/resendDlr |
Filter by startDate, endDate, senderId and messageStatus |
curl "https://api.broadnet.me/whatsapp/message/bulk-insights?batch_Id=2609091405000000001&page=0&size=100" \
-H "Authorization: Bearer $BROADNET_TOKEN"
batch_Id is the ID returned by the send call, and page is zero-based. Run a nightly reconciliation that reads the previous day's batches and fills any gaps.
Troubleshooting checklist
- Is the URL configured on the right account or sub-account?
- Is the endpoint reachable over HTTPS with a valid certificate?
- Does it answer
2xxquickly, or time out? - Is a firewall or WAF rule blocking the requests?
- Do you log the raw body before parsing, so a parser error cannot hide the event?
Worked example: a retailer's notification pipeline
"Souq Rafiq" (synthetic), a UAE retailer, sends order updates by WhatsApp and wants accurate status in its order tool.
- Orders are sent with
sendbulk, and thebatchIdis stored on the order. See send a WhatsApp message via API. - The webhook endpoint writes every event to a table, then returns
200. - A worker ranks statuses and updates the order's message state.
FAILEDevents create a task for support with the reason, and an SMS fallback is considered per channel fallback.- Inbound customer messages are routed to the shared inbox so agents can reply inside the window.
- A nightly job calls
bulk-insightsfor yesterday's batches and corrects anything missed.
What are the common mistakes?
- Heavy work before responding, which causes timeouts and repeated deliveries.
- Acknowledging before storing, which loses events when storage fails.
- Trusting order. A late
SENToverwritingDELIVERED. - Coding against the illustrative payload instead of real events.
- Re-serialising JSON before checking a signature, which changes the bytes.
- Resending from the handler, which can loop.
- Limits: this guide describes our documented behaviour. Retry policy and payload fields for your account should be confirmed during onboarding.
How OmniChannels.ai helps
OmniChannels.ai pushes delivery statuses and inbound messages for WhatsApp, SMS, RCS and other channels to your webhook in one normalised format, with separate webhooks per sub-account for resellers. BroadNet is an official Meta Business Solution Provider, so Meta's own webhook requirements are handled for you, and you work with our API. Read the webhooks reference and the WhatsApp API docs, or see how events appear in the shared inbox.
Frequently asked questions
What is a WhatsApp webhook?
A WhatsApp webhook is a URL on your server that the platform calls with an HTTP POST whenever something happens, such as a message being delivered, read or failing, or a customer sending you a message. It replaces polling. You expose an HTTPS endpoint, share it with your provider, and process the JSON events it receives.
How do I verify a WhatsApp webhook signature?
For a webhook that Meta sends directly to your own app, Meta's documentation says to compute an HMAC-SHA256 of the raw payload with your app secret and compare it with the X-Hub-Signature-256 header. For the OmniChannels.ai webhook, the API spec defines no signature header, so rely on HTTPS, an unguessable path and validating the body.
What happens if my webhook endpoint is down?
Meta's documentation says it retries failed deliveries with decreasing frequency for up to 7 days, which can produce duplicates. For events from our platform, confirm retry behaviour with your onboarding engineer, and use the bulk-insights endpoint to read per-recipient WhatsApp status for a batch. For SMS, the resendDlr endpoint re-pushes receipts.
How do I handle duplicate webhook events?
Make processing idempotent. Look up the message by its ID, compare the incoming status rank with the stored one, and ignore events that are not an advance. Applying the same event twice must have the same effect as once. Store the raw body of every event so you can replay it after fixing a bug.
How fast must my endpoint respond?
Within a few seconds. Store the raw event, return a 2xx, and do the real work in a queue or worker. Do not call other services, send messages or run heavy queries before responding. If storage fails, return a 5xx so the sender can retry, instead of acknowledging an event you lost.
Can I use a webhook to send a reply automatically?
Yes, but not from inside the handler. Acknowledge the event, then decide on the reply in a separate job. Inside the 24-hour customer service window you can send a free-form message; outside it you need an approved template. Never resend a message from the webhook handler itself, to avoid loops and duplicates.
Sources
Keep reading
More guidesIntegration
Send a WhatsApp Message via API: cURL, Node, Python, PHP
How to send a WhatsApp message with an API: working cURL, Node.js, Python and PHP examples for templates and session messages, plus status tracking and errors.
10 min read
Integration
WhatsApp CRM Integration Guide: 3 Ways to Connect and Sync
WhatsApp CRM integration explained: native connectors, middleware or API and webhooks, with data flow, field mapping, consent sync, and a checklist to choose.
10 min read
WhatsApp
Free WhatsApp Business API Messages in 2026: What Costs Nothing
Which WhatsApp Business API messages are free in 2026: 1,000 free service messages per number, the 72-hour free entry point window, and what is charged.
9 min read
