WhatsApp Cloud API webhooks
What WhatsApp sends us
When a Meta-side event happens (customer sends WA message, message delivered/read, template approved/rejected), Meta posts to Sumeru's webhook endpoint.
Topic catalog
Message events
| Event | Use |
|---|---|
messages (inbound) | Customer sent us a WA message → enqueue for AI Concierge or human |
messages.delivery | Delivered to device |
messages.read | Customer read message |
messages.failed | Delivery failed (number invalid, etc.) |
Template events
| Event | Use |
|---|---|
message_template_status_update | Template approved / rejected / paused by Meta |
phone_number_quality_update | Number quality rating changed |
Account events
| Event | Use |
|---|---|
account_alerts | Meta-flagged issues |
account_review_update | Account review status |
Webhook verification
Initial subscription handshake
Meta sends a GET with verification challenge:
GET /webhooks/whatsapp?hub.mode=subscribe&hub.challenge=12345&hub.verify_token=YOUR_TOKEN
Sumeru returns the challenge value if hub.verify_token
matches. One-time setup; happens at WA Business Account
connection.
HMAC verification on events
Every webhook POST is signed:
Header: X-Hub-Signature-256: sha256=<hex>
Body: raw JSON
import crypto from 'node:crypto';
function verifyMetaWebhook(rawBody, signature, appSecret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', appSecret)
.update(rawBody)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
} catch {
return false;
}
}
App secret is per WA Business Account; configured at connection.
Payload examples
Inbound message
{
"entry": [{
"id": "WHATSAPP_BUSINESS_ACCOUNT_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "1234567890",
"phone_number_id": "PHONE_ID"
},
"messages": [{
"from": "5551234567",
"id": "wamid.HBgM...",
"timestamp": "1715346000",
"text": { "body": "Where is my order?" },
"type": "text"
}]
}
}]
}]
}
Message status (delivery / read)
{
"entry": [{
"changes": [{
"field": "messages",
"value": {
"statuses": [{
"id": "wamid.HBgM...",
"status": "read",
"timestamp": "1715346030",
"recipient_id": "5551234567"
}]
}
}]
}]
}
Template status update
{
"entry": [{
"changes": [{
"field": "message_template_status_update",
"value": {
"event": "PAUSED",
"message_template_id": "TEMPLATE_ID",
"message_template_name": "cart_recovery_v2",
"message_template_language": "en_US",
"reason": "ABUSED_FOR_PROMOTIONAL"
}
}]
}]
}
Routing
Inbound messages route to:
- WhatsApp Engine P3 (AI Concierge) if Pro+ enabled
- Human queue if AI confidence below threshold or escalation rule matched
Status events update MessageLog:
delivered→delivered_attimestampread→opened_attimestamp (drives engagement metrics)failed→failed_at+ error code
Template status updates trigger:
PAUSED/REJECTED→ P1 anomaly + auto-pause campaigns using that templateAPPROVED→ unblock campaigns waiting on approval
Common patterns
Template paused mid-campaign
Meta paused cart_recovery_v2. Sumeru:
- Receives
PAUSEDevent - P1 anomaly fires: "WhatsApp template paused"
- All campaigns + journeys using that template auto-pause
- Merchant gets in-app + email + Slack notification
- Merchant fixes template (edit + resubmit)
- On
APPROVED, campaigns auto-resume
Number quality drop
Phone number quality drops from GREEN → YELLOW. Sumeru:
- Receives
phone_number_quality_update - P2 alert fired
- Daily-send rate auto-throttled to protect number
- Merchant prompted to investigate (often: opt-in hygiene)