Sending a WhatsApp message is only half the story. The other half — did it arrive? was it read? did it fail? — comes back asynchronously through webhooks. Here's how WhatsApp's status model works and how to consume it without shooting yourself in the foot.

The status ladder

  • accepted — WhatsApp took your send request (the 200 you get back). Meta may instead say it is holding the message for a quality check before sending.
  • sent — it left WhatsApp toward the recipient.
  • delivered — it reached the recipient's device.
  • read — they opened it (if they have read receipts on).
  • failed — it couldn't be delivered, with an error code and details.

Statuses can arrive out of order, so only ever move a message forward — one that's read shouldn't drop back to delivered. And a 200 at send time is not “delivered”; treating it that way is a common mistake.

How webhooks deliver this

Meta POSTs events to a webhook URL you register: inbound messages from customers, and status updates for messages you sent. A status update can also carry Meta's pricing details for that message (its category and whether it is charged). Two things you must get right:

  • Verify the signature. Each webhook carries an X-Hub-Signature-256 header — an HMAC-SHA256 of the body made with your app secret. Verify it over the raw body before trusting anything — otherwise anyone can POST you fake events.
  • Deduplicate. Meta retries undelivered webhooks for up to 7 days, so you'll see the same event more than once. Key on the message id and the status, and process each once.

Inbound messages matter too

Webhooks also carry the customer's replies — and each inbound message reopens their 24-hour window. So your webhook handler isn't just for analytics; it's what tells you the window is open again.

Not every sender comes with a phone number any more. People who use a WhatsApp username can keep their number private, and then the webhook identifies them by a business-scoped user ID instead. Store that ID as well as the number.

Ack fast, work later

Answer quickly with a 200 and do the heavy work afterwards. Meta expects a median response under 250 ms; anything but a 200 is treated as a failure and retried, which multiplies your load. Persist and return; process asynchronously.

What walpio does with all this

walpio runs the webhook pipeline for you: signature verification per client, deduplication, a status history per message, and inbound handling that keeps your window state correct — including customers known only by their business-scoped user ID. You read the results with GET /v1/messages/{id} or in the dashboard, or have walpio post them to your own URL as signed callbacks (message.status, message.inbound, template.status), retried if your endpoint is down. See the callbacks reference.