Skip to content

Webhooks

Webhooks can be managed in the dashboard under Settings → Webhooks, or through the API when you want them created as part of provisioning a customer.

For what the events contain, see Event reference. For handling them, see Receive messages.

The catalogue of subscribable events.

Scope: account:read

Read this rather than hard-coding a list, which goes stale the next time an event is added.

200
{
"data": [
{
"value": "message.delivered",
"group": "messages",
"label": "Message delivered",
"description": "WhatsApp confirmed the message reached the recipient's device.",
"label_key": "webhooks.events.messageDelivered.label",
"description_key": "webhooks.events.messageDelivered.description"
}
],
"has_more": false
}

The *_key fields are i18n keys used by our own dashboard. Ignore them and use label and description.

Scope: webhooks:write · Accepts Idempotency-Key

{
"name": "Order updates",
"url": "https://api.example.com/hooks/kmessage",
"events": ["message.delivered", "message.read", "message.failed"],
"headers": { "X-Internal-Auth": "..." }
}
FieldNotes
nameRequired. For your own records.
urlRequired. Public http or https. Private and internal addresses are refused.
eventsRequired, at least one. Unknown names are rejected.
headersOptional extra headers sent with every delivery.
secretOptional. One is generated if you do not supply it.
is_activeDefaults to true.
201
{
"id": "2b3c4d5e-...",
"name": "Order updates",
"url": "https://api.example.com/hooks/kmessage",
"events": ["message.delivered", "message.read", "message.failed"],
"is_active": true,
"secret": "9f8e7d6c5b4a39281706f5e4d3c2b1a0",
"created_at": "2026-08-20T10:00:00Z"
}

The secret appears only here. It is what your endpoint uses to verify that a delivery came from us, and returning it on every read would put it in every log and cache that ever touched the endpoint. Store it now. See Verify webhook signatures.

An unknown event name is refused rather than ignored, because a webhook that looks correctly configured and never fires is indistinguishable from a broken delivery pipeline.

Scope: webhooks:write

Partial: fields you omit are unchanged. Sending secret rotates it.

events cannot be set to an empty array. Set is_active to false to stop deliveries; an endpoint subscribed to nothing is one that exists, looks configured, and can never fire.

Sends a sample test event and reports what your endpoint said.

Scope: webhooks:write

200
{
"delivered": false,
"status_code": 500,
"duration_ms": 84,
"delivery_id": "8f3a1c2d-...",
"response_body": "Internal Server Error",
"error": "webhook returned non-2xx status: Internal Server Error"
}

The HTTP status is 200 even when your endpoint fails. The request to us succeeded; it was your server that answered 500, and conflating the two would lose the one fact you need.

Test deliveries appear in the history below, like any other.

Recent attempts, newest first.

Scope: webhooks:read

QueryNotes
eventRestrict to one event type
limit1 to 200, default 50
200
{
"data": [
{
"id": "1a2b3c4d-...",
"delivery_id": "8f3a1c2d-...",
"event": "message.delivered",
"attempt": 2,
"status_code": 200,
"success": true,
"duration_ms": 112,
"request_body": "{\"event\":\"message.delivered\",...}",
"response_body": "ok",
"created_at": "2026-08-20T09:15:04Z"
}
],
"has_more": false
}

Every attempt is recorded, including retries, with the exact payload we sent and what your server replied. History is kept for 7 days.

This is the first place to look when something did not arrive. All attempts for one event share a delivery_id, so a retry can be told apart from a genuinely duplicate event. A status_code of 0 means the request never completed: DNS failure, timeout, or a refused connection.

GET /v1/webhooks · DELETE /v1/webhooks/{id}

Section titled “GET /v1/webhooks · DELETE /v1/webhooks/{id}”

List and delete. Delete returns 204.