WhatsApp webhooks explained: delivery, ordering and duplicates
Webhooks arrive at least once, out of order, and sometimes not at all. What the payloads contain, how verification works, and the handling that makes a bot reliable.
On this page
- What we'll cover
- What arrives, and when
- Verification and security
- At-least-once delivery
- Building it with CodeWords
- Ordering is not guaranteed
- Responding fast, processing later
- Status events and what they mean
- When webhooks stop arriving
- Handling media, which is its own problem
- Making it survive
- Limits worth knowing about
- What to build first
- Frequently asked questions
- Related reading
A WhatsApp integration receives everything through webhooks: incoming messages, delivery statuses, template approvals, account changes. They are straightforward to accept and the reliability problems come from three properties people build around too late — they arrive at least once, not necessarily in order, and occasionally not at all.
Designing for those three from the start is most of what separates a bot that behaves correctly from one that double-replies, processes a status for a message it has not seen, and silently stops working one afternoon.
What we'll cover
- What arrives, and when
- Verification and security
- At-least-once delivery
- Building it with CodeWords
- Ordering is not guaranteed
- Responding fast, processing later
- Status events and what they mean
- When webhooks stop arriving
- Handling media, which is its own problem
- Making it survive
- Limits worth knowing about
- What to build first
- Frequently asked questions
What arrives, and when
Inbound messages from customers, including text, media, location, contacts and interactive replies.
Message status updates as an outbound message progresses — sent, delivered, read, failed.
Template status changes, including approval and rejection, which are easy to miss and worth acting on.
Account and quality notifications, including rating changes, which is the signal you most want and most often ignore.
Several events can arrive in one payload, which is a common parsing mistake — treat it as a list rather than a single event.
Subscribe only to what you handle, since unhandled event types are noise that obscures the ones you need.
Verification and security
The initial subscription handshake requires echoing a challenge token, which is a one-time setup step.
Every payload is signed, and verifying that signature is what distinguishes a genuine delivery from anybody who found your endpoint.
Verify before parsing, not after, and reject rather than logging and continuing.
Your endpoint is public, so treat every request as untrusted until the signature is checked.
Use the raw body for verification, since a parsed and re-serialised payload will not match.
Rotate the secret if it has ever been in a place it should not be, including a shared log.
At-least-once delivery
Duplicates are normal, not exceptional, and they increase when your endpoint is slow.
Key on the message id and record what you have processed.
Make every handler idempotent, so a repeat produces no additional effect.
A duplicate reply is the visible symptom, and on a personal channel it is a conspicuous one.
Store the processed ids for long enough to cover a retry window rather than forever.
Test it deliberately by replaying a payload, which is the check almost nobody runs.
Building it with CodeWords
CodeWords connects to more than 3,000 integrations, and the connection is made once and reused.
- Open CodeWords and start a new automation.
- Describe what should happen in plain language to Cody, the automation builder: which events matter, what each triggers, and what should be ignored.
- Authorize the connection to your WhatsApp Business Platform account and the systems the conversation needs.
- Describe the exceptions: a duplicate event, an out-of-order status, an unsupported message type, a failed send.
- Replay a stored payload to confirm the handler is idempotent before relying on it.
You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.
Ordering is not guaranteed
A read status can arrive before the delivered status, which breaks naive state machines.
Two messages from one customer can arrive in the opposite order to the one they were sent.
Use the timestamps in the payload rather than arrival order to sequence anything.
Make state transitions tolerant. Moving to a state you are already past should be ignored rather than treated as an error.
Never assume a status implies its predecessors have already been processed.
This is the failure that looks like a bug in your own code for a long time before anybody suspects ordering.
Responding fast, processing later
Acknowledge immediately with a success response, and do the work afterwards.
Slow responses cause retries, which is how one message becomes several duplicates.
Queue the payload and return, which is the pattern that makes everything else manageable.
Never call a slow downstream system inside the webhook handler.
Persist before acknowledging, so a crash after the acknowledgement does not lose the message.
Failing loudly is better than timing out, since a non-success response gets retried and a timeout gets retried and also looks like an outage.
Status events and what they mean
Sent means accepted by WhatsApp, not delivered to anybody.
Delivered means it reached the device.
Read requires the recipient to have read receipts enabled, so its absence means nothing.
Failed carries an error code worth handling specifically rather than generically.
A message can fail after being accepted, which is why a successful send response is not the end of the story.
Record the final status per message, because "did they get it" is the question you will be asked.
When webhooks stop arriving
Silence looks identical to a quiet period, which is the defining monitoring problem here.
Meta will disable a subscription that fails repeatedly, and the notice is easy to miss.
Certificate expiry on your endpoint is a common and entirely preventable cause.
Alert on absence, not just on errors — no inbound messages for an unusual period should page somebody.
Check the subscription status as part of a health check rather than assuming it.
Have a reconciliation path that can fetch what you missed rather than losing it.
Handling media, which is its own problem
A large share of inbound WhatsApp traffic is photographs, voice notes and documents, and none of it arrives in the webhook.
You receive an id, and must fetch the content separately using it.
The window is limited, so deferring the fetch until somebody opens a ticket is how media goes missing.
Fetch promptly and store it yourself, which is the only durable approach.
Voice notes are common in several markets and are frequently the primary way customers communicate rather than an exception.
Size and type limits apply to what customers can send and to what you can send back.
Consider what you are now storing, since customer photographs of documents, damage or people carry the obligations that come with them.
Making it survive
Verify, deduplicate, queue, acknowledge — in that order, as the standard handler shape.
Handle unknown event types without failing, since Meta adds them.
Log the payload for anything unhandled, which is how you discover a new type arrived.
Monitor endpoint latency, because it is what drives retries and duplicates.
Report the outcome: events received by type, duplicates suppressed, processing failures, and time since the last inbound message.
Limits worth knowing about
Retry behaviour is Meta's and is not configurable by you.
Payload shapes change as the platform evolves, so parsing should tolerate unknown fields.
Media is not in the payload. You receive an id and must fetch the content separately, within a limited window.
Read receipts depend on user settings, so read rate is not a metric you can rely on.
Your endpoint's availability is your problem, and a subscription disabled for failures needs manual attention.
What to build first
The handler shape: verify the signature, check the message id against what you have processed, queue the payload, acknowledge. It is perhaps twenty lines, it addresses all three of the properties that cause reliability problems on this channel, and retrofitting it after a customer has received the same reply twice is considerably less pleasant.
Two habits make the difference. Alert on the absence of inbound messages rather than only on errors, since a disabled subscription produces silence that looks exactly like a quiet afternoon. And replay a stored payload deliberately to confirm your handler is idempotent, because at-least-once delivery is documented, universally ignored, and produces the most visible possible failure on a personal messaging channel.
Frequently asked questions
What do WhatsApp webhooks deliver?
Inbound messages, message status updates, template approval and rejection, and account notifications including quality rating changes. Several events can arrive in one payload, so parse it as a list rather than a single event.
How is a webhook verified?
An initial challenge handshake at subscription, then a signature on every payload. Verify the signature against the raw body before parsing, and reject rather than logging and continuing — the endpoint is public.
Why do I get duplicate messages?
Because delivery is at least once, and duplicates increase when your endpoint is slow. Key on the message id, record what you have processed, and make every handler idempotent.
Do webhooks arrive in order?
No. A read status can arrive before delivered, and two customer messages can arrive reversed. Sequence using the timestamps in the payload, and make state transitions tolerant of arriving at a state you have already passed.
What is the right handler shape?
Verify, deduplicate, queue, acknowledge — then process afterwards. Never call a slow downstream system inside the handler, because slow responses cause retries, which cause duplicates.
What does a "sent" status actually mean?
Accepted by WhatsApp, not delivered to anybody. A message can still fail after being accepted, which is why a successful send response is not the end of the story.
Can I rely on read receipts?
No. They require the recipient to have read receipts enabled, so their absence means nothing about whether the message was read.
How do I know webhooks have stopped?
Only by alerting on absence. Meta disables subscriptions that fail repeatedly, certificate expiry on your endpoint is a common cause, and silence looks identical to a quiet period.
How do I handle images and voice notes?
The webhook carries an id rather than the content, so you must fetch it separately and promptly — the window is limited, and deferring the fetch is how media goes missing. Store it yourself if you need it later.
What should I consider before storing customer media?
That you are holding photographs customers sent of documents, damage or people. The retention and access obligations follow from the content rather than from the channel it arrived on.
Should I subscribe to every webhook event type?
Only the ones you handle. Unhandled types are noise that obscures the events you need, and a payload full of events nobody reads makes a genuine problem harder to spot.
What happens if my endpoint is slow?
Retries, which become duplicates. Acknowledge immediately and process afterwards — the queue-and-return pattern is what makes the rest of the handling manageable.