Agent skill

WhatsApp Webhooks Skill

Receive and verify WhatsApp Business Platform (Cloud API) webhooks from Meta. Use when setting up WhatsApp webhook handlers, completing the GET verification handshake, debugging X-Hub-Signature-256 signature verification, or handling inbound message and message status (sent, delivered, read, failed) events under the whatsapp_business_account object.

Install this skill

npx skills add hookdeck/webhook-skills --skill whatsapp-webhooks


Receive webhooks from the WhatsApp Business Platform (Cloud API), delivered by Meta's Graph API. WhatsApp webhooks are Meta webhooks: they require a one-time GET verification handshake and sign every POST with X-Hub-Signature-256. They do not follow the Standard Webhooks spec.

When to Use This Skill

  • How do I receive WhatsApp webhooks?
  • How do I complete the WhatsApp / Meta webhook verification handshake (hub.challenge)?
  • How do I verify the WhatsApp X-Hub-Signature-256 signature?
  • Why is my WhatsApp webhook signature verification failing?
  • How do I handle inbound WhatsApp messages vs. message status updates?

Two Things Every Endpoint Must Do

  1. GET handshake — When you register the endpoint, Meta sends a GET with hub.mode=subscribe, hub.verify_token, and hub.challenge. If the mode is subscribe and the token matches your configured verify token, respond 200 with the raw hub.challenge value as the body (no JSON, no quotes).
  2. POST signature check — Every event POST carries X-Hub-Signature-256: sha256=<hex>. Compute HMAC-SHA256 over the raw request body using your app secret and compare timing-safe.

Verification (core)

Compute HMAC-SHA256 over the raw bytes of the request body keyed on your Meta app secret, then compare against the hex digest after sha256=. Use the raw body exactly as received — Meta escapes non-ASCII characters (e.g. é), so re-serializing parsed JSON produces a different, failing digest.

Node:

const crypto = require('crypto');

function verifyWhatsAppSignature(rawBody, signatureHeader, appSecret) {
  const [algo, sig] = (signatureHeader || '').split('=');
  if (algo !== 'sha256' || !sig) return false;
  const expected = crypto.createHmac('sha256', appSecret).update(rawBody).digest('hex');
  try {
    return crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'));
  } catch {
    return false; // length mismatch = invalid
  }
}

Python:

import hmac, hashlib

def verify_whatsapp_signature(raw_body: bytes, signature_header: str, app_secret: str) -> bool:
    algo, _, sig = (signature_header or "").partition("=")
    if algo != "sha256" or not sig:
        return False
    expected = hmac.new(app_secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

Meta's official whatsapp Node SDK is built for sending messages via the Cloud API; it does not expose webhook HMAC verification, so verify manually with the standard algorithm above (see references/verification.md).

For complete handlers with the GET handshake, event dispatch, and tests, see:

Payload Shape

Every event is wrapped under the whatsapp_business_account object. The field property names the subscription (it is not a dotted event name):

{
  "object": "whatsapp_business_account",
  "entry": [{
    "id": "<WABA_ID>",
    "changes": [{
      "field": "messages",
      "value": {
        "messaging_product": "whatsapp",
        "metadata": { "phone_number_id": "..." },
        "messages": [ { "from": "...", "id": "wamid...", "type": "text", "text": { "body": "Hi" } } ],
        "statuses": [ { "id": "wamid...", "status": "delivered", "recipient_id": "..." } ]
      }
    }]
  }]
}

Dispatch by iterating entry[].changes[] and branching on change.field. For the messages field, inbound user messages arrive in value.messages[] and outbound status updates arrive in value.statuses[] — the same field carries both.

Common Subscription Fields & Events

fieldContainsNotes
messagesvalue.messages[]Inbound messages: text, image, audio, video, document, sticker, location, contacts, interactive, button, reaction, order, system
messagesvalue.statuses[]Outbound delivery receipts: sent, delivered, read, failed
message_template_status_updatevalueTemplate approved / rejected / paused
account_updatevalueBusiness account changes, bans, verification
phone_number_quality_updatevaluePhone number quality rating changes

Full reference: Webhook messages component

Environment Variables

WHATSAPP_APP_SECRET=your_meta_app_secret       # App Dashboard > App Settings > Basic > App Secret
WHATSAPP_VERIFY_TOKEN=your_own_random_string   # You choose this; must match the dashboard value

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 whatsapp --path /webhooks/whatsapp

Gotchas

  • Verify over the raw body — Meta escapes unicode; re-serialized JSON fails.
  • Dedupe by message/event id — retries (up to 7 days, decreasing frequency) go to every subscribed app, and updates may batch up to 1000 entries per POST (payloads up to 3 MB).
  • Two secrets — the app secret signs POSTs; the verify token is only for the GET handshake. They are different values.
  • Live mode — some webhooks only fire when the app is in Live mode, and a valid TLS certificate is required.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Aug 2, 2026

View on GitHub →