Receive webhooks
Rather than polling, give us a URL and we will POST events to it as they happen.
Set your callback URL
In the console, go to Settings → Console → Webhooks:
- Put your public HTTPS URL in Callback URL.
- Turn Deliver events on.
- Copy the webhook signing secret and store it with your other secrets. Your handler needs it to prove an event is ours.
There is no API for any of this — an API key cannot change where your events go.
Nothing is delivered until the endpoint is set, so no events are queued up waiting for it. Set it before you need it.
The events
| Event | Fires when |
|---|---|
message.sent | We handed the message to WhatsApp |
message.delivered | It reached the device |
message.read | The user opened it |
message.failed | It will not arrive |
inbound.received | The user replied to you |
verify.delivered | A code reached the user |
verify.approved | A code was entered correctly |
verify.failed | A verification failed |
template.status_changed | Meta approved or rejected a template |
template.category_changing | Meta will change a template's category, usually in about a day |
template.category_changed | Meta changed a template's category, including during review |
usage.threshold | A key passed 80% or 100% of its monthly limit |
inbound.received matters more than it looks: there is no endpoint that reads replies, so this
webhook is the only way to see one.
The two category events warn you before a template stops sending. template.category_changing
carries category, newCategory and effectiveAt; template.category_changed carries
previousCategory and category. template.status_changed carries category too, so an approval
says which category Meta approved it as. A template Meta moves to MARKETING can no longer be sent
through the API: a send returns template_category_not_allowed.
Every delivery is signed
The URL is public, and anyone who finds it can POST to it. Each delivery carries an
X-Msgeasy-Signature header that proves it came from us, and your handler must check it before
trusting the body.
How to check it, in each framework, is in Write a webhook handler.
The payload
{
"id": "msg:0f3a...:delivered",
"type": "message.delivered",
"sequence": 2,
"createdAt": "2026-09-06T10:35:00.000Z",
"data": { "messageId": "msg_...", "status": "delivered", "whatsappMessageId": "wamid..." }
}id is derived from the event, not random, so it is your dedupe key. sequence ranks the status.
Both matter because events can arrive twice and out of order —
Write a webhook handler shows how to handle that.
Delivery
Answer with a 2xx. A delivery that keeps failing is retried and then dropped — the schedule, and
what to check when nothing arrives, are in
Write a webhook handler.
If you need a second source of truth on a message, poll
messages.get as a backstop.