API reference

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.

Was this guide useful? Report an issue