> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hookmyapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive webhooks

> Receive WhatsApp messages at your webhook URL.

## Message body

WhatsApp events arrive as nested JSON: `entry`, `changes`, `value`, then `messages`.

```json theme={null}
{
  "object": "whatsapp_business_account",
  "entry": [
    {
      "id": "102290000000001",
      "changes": [
        {
          "field": "messages",
          "value": {
            "messaging_product": "whatsapp",
            "metadata": { "phone_number_id": "106540000000002" },
            "messages": [
              {
                "from": "15551234567",
                "id": "wamid.abc123...",
                "timestamp": "1716300000",
                "type": "text",
                "text": { "body": "hello" }
              }
            ]
          }
        }
      ]
    }
  ]
}
```

## Verification GET

When you set a deployed webhook URL, HookMyApp checks that your receiver can answer a verification request.<br />
Respond with `VERIFY_TOKEN` and HTTP 200.<br />
Local listen commands skip this check because they send messages to your computer only while the command runs.

```javascript theme={null}
app.get('/webhook/whatsapp', (req, res) => {
  res.send(process.env.VERIFY_TOKEN)
})
```

## Signature verification

Every message delivery POST arrives with `X-HookMyApp-Signature-256: sha256=<hex>`.<br />
Compute HMAC-SHA256 over the raw request body using `WEBHOOK_HMAC_SECRET` as the key.<br />
This is the HMAC signing secret from `hookmyapp channels env` or `hookmyapp sandbox env`.<br />
It is not the `VERIFY_TOKEN`.<br />
The `VERIFY_TOKEN` is only echoed back on the ownership `GET` probe.<br />
Compare the computed value against the hex digest in the header.

One POST is never signed: when you run `webhook set`, HookMyApp sends an empty-body verification probe marked `X-HookMyApp-Probe: webhook-verification` (User-Agent `HookMyApp-Webhook-Verifier`).<br />
Answer it with any 2xx before checking signatures, or verification fails.

For more detail, see [Meta payload validation docs](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks#validating-payloads).

```javascript theme={null}
import express from 'express'
import { createHmac, timingSafeEqual } from 'node:crypto'

const app = express()
const HMAC_SECRET = process.env.WEBHOOK_HMAC_SECRET
const VERIFY_TOKEN = process.env.VERIFY_TOKEN // only for the GET ownership probe

// 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) => {
    // The `webhook set` verification probe is unsigned and empty —
    // acknowledge it before the signature check. Match the full probe
    // contract (header + user agent + empty body); anything else falls
    // through to signature verification.
    if (
      req.get('X-HookMyApp-Probe') === 'webhook-verification' &&
      req.get('User-Agent') === 'HookMyApp-Webhook-Verifier' &&
      !req.body?.length
    ) {
      return res.sendStatus(200)
    }

    const signature = req.get('X-HookMyApp-Signature-256') || ''
    const expected = 'sha256=' +
      createHmac('sha256', HMAC_SECRET)
        .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

<Warning>
  Return 200 immediately.<br />
  Process asynchronously.<br />
  Queue longer work before responding.
</Warning>

## Three ways to receive messages

* **Listen in the sandbox**: `hookmyapp sandbox listen --path /webhook/whatsapp` tells HookMyApp to send test WhatsApp messages to your local receiver.<br />
  Use this with the HookMyApp test number.<br />
  Keep the CLI running while you test.
* **Listen to your own number locally**: `hookmyapp channels listen <channel>` tells HookMyApp to send messages from your connected WhatsApp number to your local receiver.<br />
  Use this to test with your real WhatsApp number before you deploy.<br />
  Stop the CLI when you are done.
* **Your own number, your own URL**: `hookmyapp channels webhook set <channel> --url <your-public-https-url>` sends messages to your deployed receiver.<br />
  Use this after your receiver is live.

## Next steps

* [Webhook routing](/whatsapp/webhook-routing): Choose where incoming messages are delivered.
* [WhatsApp quickstart](/whatsapp/quickstart): Skip the boilerplate and clone the reference receiver.
