> ## 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 Messenger messages, comment events and post events at your webhook URL.

## Message body

Facebook sends Messenger events as nested JSON: `entry`, then `messaging`.<br />
The `object` is `page` and `entry[].id` is your Page id.

```json theme={null}
{
  "object": "page",
  "entry": [
    {
      "id": "100000000000001",
      "time": 1789463829058,
      "messaging": [
        {
          "sender": { "id": "200000000000001" },
          "recipient": { "id": "100000000000001" },
          "timestamp": 1789463827321,
          "message": {
            "mid": "m_TEST_001",
            "text": "hello"
          }
        }
      ]
    }
  ]
}
```

The `sender.id` is the Page-scoped id of the person (PSID).<br />
Pass it back as the `recipient.id` when you reply.<br />
Timestamps in `messaging` are in milliseconds.

## Other message events

Beyond plain text, the `messaging` array carries these events. All arrive at the same webhook URL.

| Event             | Key fields                                                                                |
| ----------------- | ----------------------------------------------------------------------------------------- |
| Media attachments | `message.attachments[]` with `type` (`image`, `video`, `audio`, `file`) and `payload.url` |
| Your own sends    | `message.is_echo: true`, with the Page as `sender`                                        |
| Reaction          | `reaction` with `mid`, `action` (`react` or `unreact`) and the `emoji`                    |
| Message seen      | `read.watermark`: every message sent before that time was seen                            |
| Delivered         | `delivery.mids` and `delivery.watermark` for messages the person received                 |
| Postback          | `postback` with the `title` and `payload` of the button or menu item tapped               |

An echo of a message the Page sent:

```json theme={null}
{
  "sender": { "id": "100000000000001" },
  "recipient": { "id": "200000000000001" },
  "timestamp": 1789463858911,
  "message": { "mid": "m_TEST_002", "is_echo": true, "text": "hello" }
}
```

A reaction:

```json theme={null}
{
  "sender": { "id": "200000000000001" },
  "recipient": { "id": "100000000000001" },
  "timestamp": 1789463898930,
  "reaction": { "mid": "m_TEST_003", "action": "react", "emoji": "❤", "reaction": "other" }
}
```

A read receipt and a delivery receipt:

```json theme={null}
{ "sender": { "id": "200000000000001" }, "recipient": { "id": "100000000000001" }, "timestamp": 1789463868609, "read": { "watermark": 1789463868157 } }
```

```json theme={null}
{ "sender": { "id": "200000000000001" }, "recipient": { "id": "100000000000001" }, "timestamp": 1789463859613, "delivery": { "mids": ["m_TEST_D0"], "watermark": 1789463858911 } }
```

A postback:

```json theme={null}
{
  "sender": { "id": "200000000000001" },
  "recipient": { "id": "100000000000001" },
  "timestamp": 1789464087051,
  "postback": { "title": "hello", "payload": "PROBE_POSTBACK", "mid": "m_TEST_004" }
}
```

## Comment and post events

Comments and posts on the Page arrive at the same webhook URL under `changes` with `field` set to `feed`.<br />
`value.item` says what changed (`comment`, `status`, `photo`, `video`, ...) and `value.verb` how (`add`, `edited`, `hide`, `unhide`, `remove`).

A new comment on one of your posts:

```json theme={null}
{
  "object": "page",
  "entry": [
    {
      "id": "100000000000001",
      "time": 1789463926,
      "changes": [
        {
          "field": "feed",
          "value": {
            "item": "comment",
            "verb": "add",
            "comment_id": "500000000000001_500000000000002",
            "post_id": "100000000000001_500000000000001",
            "parent_id": "100000000000001_500000000000001",
            "from": { "id": "200000000000001", "name": "Test User" },
            "message": "hello",
            "created_time": 1789463921,
            "post": {
              "id": "100000000000001_500000000000001",
              "status_type": "mobile_status_update",
              "is_published": true,
              "permalink_url": "https://www.facebook.com/..."
            }
          }
        }
      ]
    }
  ]
}
```

`comment_id` feeds every comment action. `post_id` is the post it belongs to; `parent_id` is the post for a top-level comment and the parent comment for a reply.<br />
Hidden and unhidden comments arrive with the same shape and `verb` set to `hide` or `unhide`. A deleted comment arrives with `verb: "remove"` and no `message`.<br />
Comments the Page posts itself also arrive as events. Compare `from.id` with your Page id to skip them.

A new post on the Page:

```json theme={null}
{
  "field": "feed",
  "value": {
    "item": "status",
    "verb": "add",
    "post_id": "100000000000001_500000000000001",
    "from": { "id": "100000000000001", "name": "Test User" },
    "message": "hello",
    "created_time": 1789463449,
    "published": 1
  }
}
```

`created_time` on feed events is in seconds.

To act on a comment event, see [Comments](/facebook/comments).

## What counts as a message

Only inbound messages with text or attachments count toward your usage.<br />
Echoes, reactions, reads, deliveries, postbacks, comments and post events are delivered but never counted.

## 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 events to your computer only while the command runs.

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

## Signature verification

Every delivery POST arrives with `X-HookMyApp-Signature-256: sha256=<hex>`.<br />
Compute HMAC-SHA256 over the raw request body using your webhook signing secret as the key.<br />
This is the HMAC signing secret from `hookmyapp channels webhook hmac show <channel>`.<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.

```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/facebook',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    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'))
    // payload.entry[...].messaging[...] for Messenger events
    // payload.entry[...].changes[...] for comment and post events
    res.json({ status: 'ok' })
  },
)
```

## Acknowledge fast

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

## Two ways to receive events

* **Listen to your own Page locally**: `hookmyapp channels listen <channel> --path /webhook/facebook` tells HookMyApp to send your Page's events to your local receiver.<br />
  Stop the CLI when you are done.
* **Your own Page, your own URL**: `hookmyapp channels webhook set <channel> --url <your-public-https-url>` sends events to your deployed receiver.<br />
  Use this after your receiver is live.

## Next steps

* [Send messages](/facebook/send-messages): Reply within the 24-hour window.
* [Comments](/facebook/comments): Act on comment events.
