> ## 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 WhatsApp events to extra URLs, each with its own secret and event types, next to your main webhook.

## Event types

| Type                        | Sent when                                            |
| --------------------------- | ---------------------------------------------------- |
| `whatsapp.message.received` | A customer sends you a message                       |
| `whatsapp.message.status`   | A message you sent is sent, delivered, read or fails |

## Events

```json whatsapp.message.received theme={null}
{
  "id": "evt_3b1f6c2e-5d4a-5b8e-9c7f-0a1b2c3d4e5f",
  "type": "whatsapp.message.received",
  "occurredAt": "2026-09-15T10:00:00.000Z",
  "channelId": "ch_AbCdEf12",
  "channel": { "type": "whatsapp" },
  "data": {
    "messageId": "wamid.HBgMOTcyNTQ1NDM0Mzg0FQIAEhgg",
    "from": "15551234567",
    "contactName": "Dana",
    "timestamp": "2026-09-15T10:00:00.000Z",
    "messageType": "text",
    "text": "Hi there",
    "media": null,
    "interactive": null,
    "context": null
  },
  "raw": { "object": "whatsapp_business_account", "entry": [] },
  "rawOmitted": false
}
```

```json whatsapp.message.status theme={null}
{
  "id": "evt_9a8b7c6d-5e4f-5a3b-8c2d-1e0f9a8b7c6d",
  "type": "whatsapp.message.status",
  "occurredAt": "2026-09-15T10:01:40.000Z",
  "channelId": "ch_AbCdEf12",
  "channel": { "type": "whatsapp" },
  "data": {
    "messageId": "wamid.HBgMOTcyNTQ1NDM0Mzg0FQIAERgS",
    "recipient": "15551234567",
    "status": "failed",
    "timestamp": "2026-09-15T10:01:40.000Z",
    "error": { "code": 131047, "title": "Re-engagement message" }
  },
  "raw": { "object": "whatsapp_business_account", "entry": [] },
  "rawOmitted": false
}
```

`raw` is what your main webhook receives for the same event (shortened here). It is `null`, with `rawOmitted: true`, when including it would make the event larger than 256 KB.

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