Webhooks
How inbound WhatsApp messages reach your systems as signed events.
Webhooks
Webhooks are how inbound WhatsApp traffic reaches you. When someone messages one of your connected numbers, we POST a signed JSON event to an HTTPS URL you control.
They are receive-only. Sending is the REST API; nothing you return in a webhook response sends a message.
Registering an endpoint
Dashboard → Developers → Webhooks → Add endpoint. You provide an HTTPS URL and choose which events to subscribe to. We generate a signing secret, shown once.
Your URL must be publicly reachable over HTTPS. We refuse plain HTTP, URLs carrying credentials, and any host that resolves to a private, loopback, link-local or otherwise reserved address — a webhook that can reach your internal network is an SSRF vector, so the check happens at registration and again on every delivery, with the resolved IP pinned for the request.
The envelope
Every event has the same outer shape:
{
"id": "ce93fdad-1a30-4a67-a4d6-7c17e513240a",
"type": "message.received",
"api_version": "2026-07-20",
"occurred_at": "2026-07-20T11:06:18+00:00",
"data": { }
}
| Field | Meaning |
|---|---|
id |
Unique per event. Use it to deduplicate — never as a resource id in an API call. |
type |
Event name — see Event reference |
api_version |
The envelope contract this event was built against |
occurred_at |
When the event happened, not when we sent it |
data |
Event-specific payload |
occurred_at is the moment of the event itself. A delivery retried an hour later
still carries the original timestamp — it is not a fresh event just because the
attempt is fresh.
api_version moves only for a breaking envelope change. New fields and new
event types are additive and will not move it, so parse defensively: ignore keys
you do not recognise rather than failing on them.
Headers
| Header | Contents |
|---|---|
MessageVia-Event |
Event name, same as type |
MessageVia-Delivery |
Delivery id — stable across retries of the same event |
MessageVia-Timestamp |
When this attempt was signed |
MessageVia-Signature |
v1=<hex hmac> — see Signature verification |
MessageVia-Delivery stays the same across every retry of one event, which makes
it the right key for "have I already processed this?".
Responding
Return any 2xx as soon as you have stored the event. Anything else — or no response within 8 seconds — counts as a failure and starts the retry clock.
Do your real work after responding. A handler that charges a card, calls three APIs and then returns 200 will eventually exceed the timeout, and we will retry an event you already handled.
receive → verify signature → store → return 200 → process asynchronously
Deduplicate
We guarantee at-least-once delivery, not exactly-once. A network blip after your 200 leaves us believing the attempt failed, and we will send it again.
Key on MessageVia-Delivery (or the envelope id) and make your handler
idempotent. Assume every event will arrive twice at some point, because
eventually one will.
Ordering
Events are not ordered. Two messages sent a second apart may arrive in either
order, and a retry can land after a newer event. Use occurred_at when order
matters — do not infer it from arrival.
Read next
- Event reference — every event and its payload
- Signature verification — verifying that we sent it
- Retries and redelivery — what happens when your endpoint fails