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

The payload shape

Instagram sends messaging events as nested entry then messaging JSON (the Messenger-style envelope, not the WhatsApp changes/value shape). HookMyApp forwards the payload byte-for-byte.
{
  "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). Pass it back as the recipient.id when you reply.

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 an Instagram receiver the URL ends in /webhook/instagram, so the probe is GET /webhook/instagram). 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/instagram', (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 webhook 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/instagram',
  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[...].messaging[...].message
    res.json({ status: 'ok' });
  },
);

Acknowledge fast

Return 200 immediately. Process asynchronously. A slow or failing handler is recorded as a failed delivery (visible in the HookMyApp Deliveries panel). Retry behavior depends on the path: the sandbox ACKs Meta right away and does NOT retry a downstream failure, so a failed forward is logged but not re-sent. With your own account, your receiver’s own response is what Meta sees, so a non-200 or a timeout can make Meta retry on its own schedule. Either way, queue work to a background job and return 200 fast.

Three ways to receive messages

  • Sandbox tunnel: hookmyapp sandbox listen --path /webhook/instagram opens a secure tunnel from a HookMyApp-managed hostname to localhost:3000/webhook/instagram. Test account, no Meta paperwork. The tunnel lives as long as the CLI runs. (--path defaults to /webhook; pass /webhook/instagram so the tunnel reaches the per-channel route above. The starter kit serves /webhook/whatsapp and /webhook/instagram side by side, so each channel uses its own --path.)
  • Your own account via tunnel: hookmyapp channels listen <channel> --path /webhook/instagram does the same for a connected Instagram account. Use this to develop locally against your real Instagram account. Stop the CLI and the tunnel is reclaimed; Meta’s webhook URL reverts to whatever was set before.
  • Your own account, 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