> ## 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.

# Event subscriptions

> Send Instagram DMs and comments to extra URLs, each with its own secret and event types, next to your main webhook.

## Event types

| Type                         | Sent when                                         |
| ---------------------------- | ------------------------------------------------- |
| `instagram.message.received` | Someone sends your account a DM                   |
| `instagram.comment.received` | Someone comments on your post, reel or live video |

A comment that tags your account arrives as `instagram.comment.received` with `data.mentionsChannel: true`. Your account's own replies are not sent.

## Events

```json instagram.message.received theme={null}
{
  "id": "evt_1c2d3e4f-5a6b-5c7d-8e9f-0a1b2c3d4e5f",
  "type": "instagram.message.received",
  "occurredAt": "2026-09-15T10:00:00.123Z",
  "channelId": "ch_IgAbCd12",
  "channel": { "type": "instagram" },
  "data": {
    "messageId": "aWdfZAG1faXRlbToxOkl",
    "from": "1234567890",
    "timestamp": "2026-09-15T10:00:00.123Z",
    "text": "Is this still available?",
    "attachments": [],
    "quickReplyPayload": null
  },
  "raw": { "object": "instagram", "entry": [] },
  "rawOmitted": false
}
```

```json instagram.comment.received theme={null}
{
  "id": "evt_7f6e5d4c-3b2a-5f1e-9d8c-7b6a5f4e3d2c",
  "type": "instagram.comment.received",
  "occurredAt": null,
  "channelId": "ch_IgAbCd12",
  "channel": { "type": "instagram" },
  "data": {
    "commentId": "17900000000000001",
    "mediaId": "18000000000000001",
    "from": { "id": "2345678901", "username": "dana.k" },
    "text": "Love this @acme.store",
    "timestamp": null,
    "parentCommentId": null,
    "isLive": false,
    "mentionsChannel": true
  },
  "raw": { "object": "instagram", "entry": [] },
  "rawOmitted": false
}
```

`occurredAt` is `null` when Instagram sends no time for the event. `mediaId` is `null` for comments on a live video.

## Create a subscription

```bash theme={null}
curl -X POST https://api.hookmyapp.com/channels/ch_AbCdEf12/subscriptions \
  -H "Authorization: Bearer hmok_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/new-message","events":["whatsapp.message.received"],"label":"New message flow"}'
```

The response is `201` with `subscriptionId` (`sub_...`) and `secret`. The secret is shown only once: store it. To change the URL or rotate the secret, delete the subscription and create a new one.

* `url`: a public `https://` address.
* `events`: at least one event type the channel supports.
* `label`: optional, up to 120 characters, shown in your delivery log.
* Up to 25 subscriptions per channel.
* Facebook Page channels do not support event subscriptions yet.

## List and delete

* `GET /channels/{ch}/subscriptions` lists a channel's subscriptions, without secrets.
* `DELETE /channels/{ch}/subscriptions/{sub}` removes one. Deleting it again is harmless. A delivery already in progress may still arrive once.

## Verify the signature

Every delivery carries `X-HookMyApp-Signature-256: sha256=<hex>`: an HMAC-SHA256 of the raw request body, keyed with the subscription's secret. It works exactly like [your main webhook's signature](/whatsapp/receive-webhooks#signature-verification).

```js theme={null}
import crypto from 'node:crypto';

function isSigned(rawBody, header, secret) {
  const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return typeof header === 'string' && header.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
```

## Delivery and retries

* Events usually arrive within seconds.
* Answer with any `2xx` within 10 seconds.
* Any other answer, a redirect or a timeout is retried with growing delays for about 3 hours. After that the event is dropped for that subscription.
* While one subscription's endpoint keeps failing, deliveries to other subscriptions can be briefly delayed.
* An event can arrive more than once, and events can arrive out of order. Drop duplicates by `id`; order by `occurredAt`.
* Every attempt shows in your delivery log with the subscription's label.
* Your main webhook destination does not change. It keeps receiving what it receives today, and a failing subscription never affects it.

## Sample events

`GET /channels/{ch}/events?types=whatsapp.message.received&limit=3` returns recent events in the same shape, one per `id`, with `raw` set to `null` and `rawOmitted` set to `true`. It returns up to `limit` of the most recent events, newest first, and it looks through a bounded number of recent deliveries, so it can return fewer than `limit` events when the type you asked for is rare. Use it as sample data when you set up an automation, not as a history feed.

## For automation platform builders

If you build a trigger for an automation platform on top of HookMyApp:

* **Subscribe** (a user turns the trigger on): `POST /channels/{ch}/subscriptions` with the event type the trigger listens for and your platform's target URL. Store the returned `subscriptionId` (and the `secret`, to verify deliveries).
* **Unsubscribe** (the trigger is turned off or deleted): `DELETE /channels/{ch}/subscriptions/{subscriptionId}`.
* **Sample data** (while the user sets up the trigger): `GET /channels/{ch}/events?types=<event type>&limit=3`.
* **Deduplication:** use the event `id`. The same event always has the same `id`, including when it is delivered more than once.

## Errors

| Code                             | Status | Meaning                                                              |
| -------------------------------- | ------ | -------------------------------------------------------------------- |
| `CHANNEL_ACTION_INPUT_INVALID`   | 400    | Body is malformed, `events` is empty or unknown, `label` is too long |
| `CHANNEL_EVENT_TYPE_UNSUPPORTED` | 400    | The channel cannot produce that event type                           |
| `WEBHOOK_URL_INVALID`            | 400    | The URL is not `https` or not a public address                       |
| `SUBSCRIPTION_NOT_FOUND`         | 404    | No such subscription on this channel                                 |
| `SUBSCRIPTION_LIMIT_REACHED`     | 409    | The channel already has 25 subscriptions                             |
