A webhook is a URL you own that Customer Pro calls. When something happens in your workspace — a customer replies, a message is read, a contact is created — the platform queues an HTTP POST to that URL with a JSON body describing the event, signed so you can prove it came from us.
Registering an endpoint #
-
Name it and give the URL
The URL must be http or https and reachable from the public internet. -
Tick the events
At least one is required.message.receivedis pre-selected because it is what most integrations want. -
Copy the signing secret
It appears once, immediately after creation, and is never shown again. Without it you cannot verify any delivery. -
Send a test
The Test action queues one delivery to your endpoint carrying"test": truein its data, so you can confirm the plumbing before real traffic arrives.
Only owners and admins can register, edit, test or delete webhooks. Everyone else sees the list read-only.
The ten event types #
| Field | What it does |
|---|---|
| message.received | An inbound message arrived from a customer. |
| message.sent | An outbound message was accepted by WhatsApp. |
| message.delivered | WhatsApp confirmed delivery to the handset. |
| message.read | The recipient opened the message. |
| message.failed | WhatsApp rejected or could not deliver the message. |
| phone.verified | A phone number completed verification. |
| conversation.created | A new conversation thread was opened. |
| conversation.updated | An existing thread changed — status, assignment or last activity. |
| contact.created | A contact was added to your workspace. |
| contact.updated | An existing contact's details changed. |
| lead.created | Someone submitted a Facebook lead ad form on a connected page. Fires after the lead has been read back from Meta, so data.fields carries the answers, not just an id. |
That list is fixed; anything outside it is rejected when the webhook is saved.
What arrives at your endpoint #
Every delivery is a JSON POST with three headers worth reading: X-Webhook-Event names the event, X-Webhook-Signature carries the signature, and the user agent is WhatsAppBusiness-Webhook/1.0. The body always has the same envelope — the event name, an ISO 8601 timestamp, your organization id, and a data object whose shape depends on the event.
{
"event": "message.received",
"timestamp": "2026-01-30T09:14:22+00:00",
"organization_id": 12,
"data": { }
}
Verifying the signature #
The signature is an HMAC SHA-256 of the raw request body, keyed by your webhook's signing secret. Compute the same thing on your side and compare with a constant-time function. Do this before you parse or trust anything in the payload — an unverified POST is just a stranger's HTTP request.
$body = file_get_contents('php://input');
$expected = hash_hmac('sha256', $body, $secret);
$given = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if (! hash_equals($expected, $given)) {
http_response_code(401);
exit;
}
Delivery, retries and timeouts #
Any 2xx response counts as delivered and clears the webhook's last error. Anything else — a 500, a connection failure, or no answer inside the timeout — is recorded as a failed attempt and retried with an exponential backoff of 2, 4, then 8 seconds.
| Field | What it does |
|---|---|
| retry_count | How many retries follow a failed first attempt. Defaults to 3, and may be set between 0 and 5. |
| timeout_seconds | How long we wait for your response before giving up on an attempt. Defaults to 10, and may be set between 5 and 30. |
The registration form uses those defaults; both values can be changed on an existing webhook. Because retries are queued rather than run inline, a slow endpoint never blocks the message that triggered the event — but it does mean deliveries only leave once a queue worker is running.
The list shows a running Deliveries count per webhook, and the most recent failure in red under the status as Last error. That string is the HTTP status we got back, or the connection error we hit — the first place to look when events stop arriving. Disabling a webhook stops delivery without deleting its history; deleting it is permanent.
Muting a webhook for one contact #
Some contacts should not be mirrored into your own systems — an internal tester, a VIP whose thread stays inside the inbox, a number under a data request. Per-contact preferences let you switch one webhook off for one contact while leaving it on for everyone else.
These are session-authenticated JSON endpoints rather than a screen. A GET to developer/contacts/{contact}/webhook-preferences returns every active webhook with its is_enabled flag for that contact, and a PATCH to developer/contacts/{contact}/webhook-preferences/{webhook} with is_enabled sets it.