Webhooks and events
The events you can subscribe to, how deliveries are signed, retried and logged.
There are two kinds of webhooks, and you only run one of them. Meta's webhooks for your WhatsApp Business Account are managed by Wabi: we subscribe to them when you connect, verify every delivery's signature, store it before processing, drop duplicates and handle out-of-order statuses. Customer webhooks are ours to you: signed events about your messages, conversations, templates and agents, delivered to HTTPS endpoints you add in Integrations. This page is that contract.
Events
| Type | When |
|---|---|
| message.received | A customer sent a message to one of your numbers (text, media, location, interactive reply, reaction). |
| message.sent | Meta accepted an outbound message for delivery. |
| message.delivered | An outbound message reached the customer's phone. |
| message.read | The customer read an outbound message (when their read receipts are on). |
| message.failed | An outbound message failed, with Meta's error code and whether a retry could help. |
| conversation.needs_reply | A customer turn is waiting for a substantive answer from your business. |
| conversation.escalated | An AI agent or a rule handed the conversation to a person, with the reason and a summary. |
| conversation.resolved | The conversation was marked resolved. |
| window.expiring | A customer's 24-hour service window closes soon; after it, only templates can be sent. |
| template.approved | Meta approved a template in one language. |
| template.rejected | Meta rejected a template in one language, with the reason. |
| template.quality_changed | Meta changed a template's quality rating or paused or disabled it. |
| flow.submitted | A customer completed a WhatsApp Flow; the payload carries their answers. |
| location.received | A customer shared a location (a single pin; continuous live location is not available on the Cloud API). |
| agent.session.completed | An AI agent session ended: resolved, escalated or timed out, with tokens and cost. |
| agent.execution.failed | An AI agent turn failed (model or tool error); the conversation is left for a person. |
New event types may be added at any time; ignore types you do not know. Existing types are never renamed or removed.
Payload
Every delivery is a POST with a JSON body: the event's id, type, occurred_at, your org_id, the subject it is about and the event's data.
{ "id": "evt_01JB7X4Q2W9N3K5R8T6Y", "type": "message.received", "occurred_at": "2026-10-23T09:41:07.000Z", "org_id": "6f1c2e0a-4b7d-4c2e-9a51-0d3b8e7f9a10", "subject": { "type": "conversation", "id": "0b8d4e2c-1f3a-4a57-9c6e-2d7f5b1a8e90" }, "data": { "message_id": "4c2f9a1e-7d3b-4e8a-b5c6-1a2d3e4f5a6b", "wamid": "wamid.HBgLMTU1NTAxMDAwMDEVAgASGBQzQTk…", "phone_number_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d", "contact": { "id": "3e2d1c0b-9a8f-4e7d-b6c5-a4b3c2d1e0f9", "wa_id": "15550100001", "name": "Sandbox customer" }, "type": "text", "text": "Hi, where is my order?", "window_expires_at": "2026-10-24T09:41:07.000Z" }}Signing
Each attempt carries three headers:
webhook-id: The event id. The same on every retry of one event: use it to drop duplicates.webhook-timestamp: Unix seconds when this attempt was sent. Reject attempts older than 5 minutes.webhook-signature: v1,<base64 HMAC-SHA256 of `${id}.${timestamp}.${body}`>, space-separated when a secret is being rotated.
Verify the signature over the raw body, before parsing it, with the endpoint's secret (shown once when you add the endpoint, rotatable in Integrations):
import { createHmac, timingSafeEqual } from "node:crypto";
// secret: the endpoint's signing secret from Integrations (whsec_…)export function verify(body: string, headers: Headers, secret: string): boolean { const id = headers.get("webhook-id"); const timestamp = headers.get("webhook-timestamp"); const signatures = headers.get("webhook-signature") ?? ""; if (!id || !timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest(); return signatures.split(" ").some((entry) => { const [version, signature] = entry.split(","); const given = Buffer.from(signature ?? "", "base64"); return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected); });}Delivery and retries
- Endpoints are HTTPS URLs added in Integrations in the dashboard, each with its own signing secret and an optional list of event types (none: every event).
- Delivery is at least once: the same event can arrive more than once, so deduplicate on webhook-id. Order is not guaranteed; use occurred_at.
- Answer with any 2xx within 10 seconds and do slow work afterwards. Anything else, or no answer, is retried with exponential backoff.
- After the last retry the delivery is marked dead. An endpoint that keeps failing is marked failing and its owners are told; every attempt, with its status code and latency, is in the endpoint's delivery log.
- Signatures follow the Standard Webhooks specification, so its libraries verify Wabi deliveries as they are.