Skip to main content

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

EventUse
messages (inbound)Customer sent us a WA message → enqueue for AI Concierge or human
messages.deliveryDelivered to device
messages.readCustomer read message
messages.failedDelivery failed (number invalid, etc.)

Template events

EventUse
message_template_status_updateTemplate approved / rejected / paused by Meta
phone_number_quality_updateNumber quality rating changed

Account events

EventUse
account_alertsMeta-flagged issues
account_review_updateAccount 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:

  1. WhatsApp Engine P3 (AI Concierge) if Pro+ enabled
  2. Human queue if AI confidence below threshold or escalation rule matched

Status events update MessageLog:

  • delivereddelivered_at timestamp
  • readopened_at timestamp (drives engagement metrics)
  • failedfailed_at + error code

Template status updates trigger:

  • PAUSED / REJECTED → P1 anomaly + auto-pause campaigns using that template
  • APPROVED → unblock campaigns waiting on approval

Common patterns

Template paused mid-campaign

Meta paused cart_recovery_v2. Sumeru:

  1. Receives PAUSED event
  2. P1 anomaly fires: "WhatsApp template paused"
  3. All campaigns + journeys using that template auto-pause
  4. Merchant gets in-app + email + Slack notification
  5. Merchant fixes template (edit + resubmit)
  6. On APPROVED, campaigns auto-resume

Number quality drop

Phone number quality drops from GREENYELLOW. Sumeru:

  1. Receives phone_number_quality_update
  2. P2 alert fired
  3. Daily-send rate auto-throttled to protect number
  4. Merchant prompted to investigate (often: opt-in hygiene)

See also