Skip to main content
HookMyApp forwards Meta’s payload signed with your VERIFY_TOKEN. Verify the HMAC, acknowledge fast, process async.

The payload shape

Meta sends WhatsApp events as nested entry then changes then value then messages JSON. HookMyApp forwards the payload byte-for-byte.
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "1276334778010256",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "whatsapp",
            "metadata": { "phone_number_id": "1080996501762047" },
            "messages": [
              {
                "from": "15551234567",
                "id": "wamid.abc123...",
                "timestamp": "1716300000",
                "type": "text",
                "text": { "body": "hello" }
              }
            ]
          }
        }
      ]
    }
  ]
}

The verification GET

When you register your own public URL with hookmyapp channels webhook set <channel> --url <your-url>, HookMyApp sends that URL a one-time GET to prove you own it (for a WhatsApp receiver the URL ends in /webhook/whatsapp, so the probe is GET /webhook/whatsapp). Respond with the raw VERIFY_TOKEN string and HTTP 200. The CLI tunnel commands hookmyapp sandbox listen and hookmyapp channels listen do NOT issue this probe; they open a forwarding tunnel and stream inbound POSTs to your local app. Keep the GET handler so your receiver verifies cleanly once you deploy it and set it as your own URL.
app.get('/webhook/whatsapp', (req, res) => {
  res.send(process.env.VERIFY_TOKEN);
});

Signature verification

Every POST arrives with X-HookMyApp-Signature-256: sha256=<hex>. Compute HMAC-SHA256 over the raw request body using VERIFY_TOKEN as the key. Compare against the header’s hex digest. The algorithm matches Meta’s webhook verification scheme, adapted for the HookMyApp forwarding path. For the underlying scheme see Meta’s payload validation docs.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();
const VERIFY_TOKEN = process.env.VERIFY_TOKEN;

// Capture the raw body. express.json() would re-serialize and
// break the HMAC comparison.
app.post(
  '/webhook/whatsapp',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.get('X-HookMyApp-Signature-256') || '';
    const expected = 'sha256=' +
      createHmac('sha256', VERIFY_TOKEN)
        .update(req.body)
        .digest('hex');

    const a = Buffer.from(signature);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !timingSafeEqual(a, b)) {
      return res.sendStatus(401);
    }

    const payload = JSON.parse(req.body.toString('utf8'));
    // Process payload.entry[...].changes[...].value.messages[...]
    res.json({ status: 'ok' });
  },
);

Acknowledge fast

Return 200 immediately. Process asynchronously. If your handler takes longer than 20 seconds, HookMyApp treats the delivery as failed and retries. Queue work to a background job before responding.

Three ways to receive messages

  • Sandbox tunnel: hookmyapp sandbox listen --path /webhook/whatsapp opens a secure tunnel from a HookMyApp-managed hostname to localhost:3000/webhook/whatsapp. Test number, no Meta paperwork. The tunnel lives as long as the CLI runs. (--path defaults to /webhook; pass /webhook/whatsapp so the tunnel reaches the route above. The starter kit serves /webhook/whatsapp and /webhook/instagram side by side, so each channel uses its own --path.)
  • Your own number via tunnel: hookmyapp channels listen <channel> does the same for a connected WhatsApp number. Use this to develop locally against your real WhatsApp number. Stop the CLI and the tunnel is reclaimed; Meta’s webhook URL reverts to whatever was set before.
  • Your own number, your own URL: hookmyapp channels webhook set <channel> --url <your-public-https-url> writes the URL to Meta’s override_callback_uri via the Graph API. Persists across CLI restarts. The URL must be public HTTPS with a valid certificate. Use once your receiver is deployed.

Next steps