WhatsApp Webhooks - Delivery, Read and Reply Events
Get WhatsApp delivered, read, failed and reply events pushed to your server. Create webhook subscriptions by API and verify each event's HMAC-SHA256 signature.
Overview
WhatsApp webhooks push events to your server as they happen, so you do not have to poll for message status. ChatMaxima sends an HTTPS POST to your URL when a WhatsApp message you sent through the API is delivered, read or failed, and when the customer replies.
You manage subscriptions with the API: create one per endpoint, choose the events, and test it before going live.
Prerequisites
- An API key on a plan with API access (Pro or higher). See ChatMaxima API.
- A public HTTPS endpoint that accepts
POSTrequests with a JSON body. - Messages sent through the API. Events cover messages sent with the WhatsApp Templates API, the WhatsApp Messages API (with a
reference_id) and the Send Messages API.
Events
| Event | Sent when |
|---|---|
delivered | The message reached the customer's phone. |
read | The customer opened the message, or played a voice note. |
failed | WhatsApp could not deliver the message after accepting it. |
reply | A customer you have messaged through the API sends you a message. |
sent | Accepted when subscribing. The send response already confirms sent, so rely on the response rather than this event. |
Status events only move forward: you will not get delivered replacing a read. Messages that Meta rejects immediately do not produce a webhook, because the send request itself returns the error.
Create a webhook
curl -X POST https://chatmaxima.com/api/v2/whatsapp/webhooks/ \
-H "Authorization: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"callback_url": "https://example.com/hooks/chatmaxima",
"events": ["delivered", "read", "failed", "reply"],
"account_id": "a1b2c3d4e5f6"
}'
| Field | Type | Required | Description |
|---|---|---|---|
callback_url | string | Yes | Your HTTPS endpoint. It must resolve to a public IP address. |
events | array | Yes | Any of sent, delivered, read, failed, reply. |
account_id | string | No | Only send events for this WhatsApp account. Leave it out to cover all of them. |
The response contains the signing secret. It is shown only once, so store it securely right away.
{
"status": "success",
"code": 201,
"message": "Webhook created. Store the secret now; it is not shown again.",
"data": {
"webhook_id": "whk_0123456789abcdef",
"callback_url": "https://example.com/hooks/chatmaxima",
"events": ["delivered", "read", "failed", "reply"],
"account_id": "a1b2c3d4e5f6",
"status": "active",
"secret": "whsec_XXXXXXXX"
}
}
List, test and delete webhooks
| Action | Request |
|---|---|
| List webhooks | GET /api/v2/whatsapp/webhooks/ |
| Send a test event | POST /api/v2/whatsapp/webhooks/{webhook_id}/test/ |
| Delete a webhook | DELETE /api/v2/whatsapp/webhooks/{webhook_id}/ |
- List returns every subscription with its
status(activeorinactive). The secret is never shown again. - Test sends a signed
webhook.testevent to your URL right away and reports what your server answered:delivered,http_statusand the start of the response body. - Delete deactivates the subscription and returns
204with no body. Pending retries for it stop.
To change a URL or the event list, create a new webhook and delete the old one.
Event payloads
Status event
{
"id": "evt_0123456789abcdef",
"event": "message.delivered",
"occurred_at": "2026-11-03T10:00:05+00:00",
"data": {
"message_id": "wamid.XXXXXXXX",
"account_id": "a1b2c3d4e5f6",
"template_name": "order_update",
"broadcast_name": null,
"reference_id": "order-1001",
"status": "delivered",
"error": null
}
}
event is message.delivered, message.read or message.failed. For failed, error holds the report WhatsApp sent, including Meta's error code. For session messages, template_name and broadcast_name are empty strings.
Reply event
{
"id": "evt_0123456789abcdef",
"event": "message.reply",
"occurred_at": "2026-11-03T10:01:00+00:00",
"data": {
"message_id": "wamid.INBOUND",
"account_id": "a1b2c3d4e5f6",
"mobile_no": "a1b2c3d4e5f6_15550001111",
"text": "Yes, confirmed",
"reference_id": "order-1001",
"broadcast_name": "",
"template_name": "order_update"
}
}
mobile_nois ChatMaxima's contact reference: the account id, an underscore, then the customer's WhatsApp number (or their WhatsApp username id).textis the message text, or the caption of a photo or file. It is empty for media sent without a caption.reference_id,template_nameandbroadcast_namecome from the most recent API message you sent this customer.
Verify the signature
Every request carries these headers:
| Header | Value |
|---|---|
X-CM-Event | The event name, for example message.read. |
X-CM-Signature | sha256= followed by the HMAC-SHA256 of the raw request body, keyed with your webhook secret. |
User-Agent | ChatMaxima-Webhook/1.0 |
Compute the HMAC over the raw body bytes before parsing the JSON, and compare it in constant time:
const crypto = require('crypto');
function is_valid_signature(raw_body, signature_header, secret)
{
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(raw_body).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature_header || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
$raw_body = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw_body, $webhook_secret);
$is_valid = hash_equals($expected, $_SERVER['HTTP_X_CM_SIGNATURE'] ?? '');
The signature does not include a timestamp, so store each event id and ignore ones you have already processed.
Delivery and retries
- Timing: events are sent within about a minute.
- Success: any
2xxresponse. Answer quickly and do slow work in the background. The request times out after 10 seconds. - Redirects are not followed, so a
3xxcounts as a failure. Use your final URL. - Retries: a failed delivery is retried after 1 minute, 5 minutes, 30 minutes and 2 hours, for 5 attempts in total. A retry carries the same event
id. - Ordering is not guaranteed. A retried
deliveredcan arrive afterread, so use the latest status your system has seen.
Errors
| HTTP | Code | When it happens |
|---|---|---|
| 422 | MISSING_CALLBACK_URL | callback_url is missing. |
| 422 | INVALID_CALLBACK_URL | The URL is not HTTPS, is localhost, does not resolve, or points to a private address. |
| 422 | MISSING_EVENTS | events is missing or empty. |
| 422 | INVALID_EVENT | An event name is not supported. |
| 422 | INVALID_ACCOUNT | account_id is not one of your WhatsApp accounts. |
| 404 | NOT_FOUND | No webhook with that webhook_id. |
Authentication, plan and rate-limit errors are listed on the API overview.
Troubleshooting
My endpoint receives no status events
- Check the message was sent through the API. Messages sent from the Inbox or by bots without a webhook do not produce these events.
- For WhatsApp session messages, check the send included a
reference_id. - Run the test endpoint and look at
http_statusand the response it reports.
Signature checks fail
Make sure you hash the raw body exactly as received. Frameworks that parse and re-serialize JSON change the bytes and break the signature.