> ## 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 Instagram DMs and comment events at your webhook URL.

## Message body

Instagram sends messaging events as nested JSON: `entry`, then `messaging`.<br />
This body is different from the WhatsApp `changes` and `value` shape.<br />

```json theme={null}
{
  "object": "instagram",
  "entry": [
    {
      "id": "17841400000000000",
      "time": 1716300000,
      "messaging": [
        {
          "sender": { "id": "17841400000000001" },
          "recipient": { "id": "17841400000000000" },
          "timestamp": 1716300000,
          "message": {
            "mid": "aWdfZG1...",
            "text": "hello"
          }
        }
      ]
    }
  ]
}
```

The `sender.id` is the Instagram-scoped sender id (IGSID).<br />
Pass it back as the `recipient.id` when you reply.

## Comment events

Comments on your posts arrive at the same webhook URL with `field` set to `comments` (or `live_comments` during a live video).<br />
Meta currently delivers comment events in **two body shapes**. Handle both.

Shape 1 — `changes` array (Meta's self-comment example):

```json theme={null}
{
  "object": "instagram",
  "entry": [
    {
      "id": "17841400000000000",
      "time": 1716300000,
      "changes": [
        {
          "field": "comments",
          "value": {
            "id": "17900000000000000",
            "text": "Nice post!",
            "from": { "id": "17841400000000001", "username": "commenter" },
            "media": { "id": "17850000000000000", "media_product_type": "FEED" },
            "parent_id": "17890000000000000"
          }
        }
      ]
    }
  ]
}
```

Shape 2 — flat `field` and `value` on the entry (Meta's ordinary-comment example):

```json theme={null}
{
  "object": "instagram",
  "entry": [
    {
      "id": "17841400000000000",
      "time": 1716300000,
      "field": "comments",
      "value": {
        "id": "17900000000000000",
        "text": "Nice post!",
        "from": { "username": "commenter" },
        "media": { "id": "17850000000000000", "media_product_type": "FEED" }
      }
    }
  ]
}
```

Treat `from.id`, `parent_id`, and fields that only appear on your own comments as optional.<br />
Some deliveries omit `from.id` entirely, and `parent_id` is only present on replies.<br />
Comments you post yourself also arrive as events — check `from.username` against your own handle if you want to skip your own replies.

Mentions of your account arrive inside these same `comments` events. There is no separate mentions field under Instagram Login.

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

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

```javascript theme={null}
app.get('/webhook/instagram', (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 webhook docs](https://developers.facebook.com/docs/graph-api/webhooks/getting-started).

```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/instagram',
  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[...].messaging[...].message
    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/instagram` tells HookMyApp to send test Instagram DMs to your local receiver.<br />
  Use this with the HookMyApp test Instagram account.<br />
  Keep the CLI running while you test.
* **Listen to your own account locally**: `hookmyapp channels listen <channel> --path /webhook/instagram` tells HookMyApp to send DMs from your connected Instagram account to your local receiver.<br />
  Use this to test with your real Instagram account before you deploy.<br />
  Stop the CLI when you are done.
* **Your own account, your own URL**: `hookmyapp channels webhook set <channel> --url <your-public-https-url>` sends DMs to your deployed receiver.<br />
  Use this after your receiver is live.

## Next steps

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