> ## Documentation Index
> Fetch the complete documentation index at: https://docs.instantcampaign.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks API

> Get workspace events POSTed to your endpoint — signed and retried when registered in Settings, or as simple REST hooks for Zapier and Make.

There are two ways to receive events. They differ in how they are delivered.

|                                     | Settings → Webhooks                                                               | REST hook (`POST /v1/webhooks/subscribe`)                   |
| ----------------------------------- | --------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Set up by                           | An owner or admin in the dashboard                                                | Your code, with an API key that has `webhooks:manage`       |
| Events                              | Several per endpoint                                                              | One per subscription                                        |
| URL                                 | Must be `https://`                                                                | Any public URL (private and internal addresses are refused) |
| Signature                           | Standard Webhooks, when you give the endpoint a secret                            | None                                                        |
| Retries                             | 3 attempts within seconds, then a schedule of about 34 hours (10 attempts in all) | The same schedule                                           |
| Disabled after 12 hours of failures | Yes                                                                               | No                                                          |
| `410 Gone`                          | Retires the endpoint                                                              | Deletes the subscription                                    |

Both send the same JSON body:

```json theme={null}
{
  "id": "msg_…",
  "event": "EMAIL_BOUNCED",
  "timestamp": "2026-09-10T09:30:00.000Z",
  "teamId": "…",
  "data": { "email": "jane@example.com", "messageId": "…", "provider": "postmark", "type": "Permanent", "reason": "550", "occurredAt": "2026-09-10T09:29:58.000Z" }
}
```

Your endpoint has 10 seconds to answer with any `2xx`. `GET /v1/webhooks/events` lists every event type you can subscribe to.

For double opt-in, `LIST_SUBSCRIPTION_CONFIRMED` is sent once for each list a confirmation click activates, with `data` of `{ "contactId", "email", "listId", "listName" }`. `CONTACT_CONFIRMED` (`{ "contactId", "email" }`) is sent only when the contact itself goes from pending to active. See [Webhooks](/guides/developers/webhooks#double-opt-in-confirmations).

## Subscribing a REST hook

```bash theme={null}
curl -X POST https://instantcampaign.ai/api/v1/webhooks/subscribe \
  -H "Authorization: Bearer ic_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "event": "CONTACT_CREATED", "targetUrl": "https://hooks.example.com/instantcampaign" }'
```

The response is `201 { "id": "…" }`. Subscribing the same event and URL again returns the same `id`. To remove the subscription, call `DELETE /v1/webhooks/{id}`.

## Verifying signatures

Endpoints registered under **Settings → Webhooks** with a secret receive these headers:

* `webhook-id`: the message ID. It stays the same across retries, so use it to deduplicate.
* `webhook-timestamp`: Unix seconds.
* `webhook-signature`: `v1,<base64 HMAC-SHA256>` of `"{webhook-id}.{webhook-timestamp}.{raw body}"`.
* `X-Signature-256`: the legacy header, `sha256=<hex HMAC-SHA256 of the raw body>`.

If the secret starts with `whsec_`, base64-decode the rest to get the HMAC key. Otherwise the key is the secret's UTF-8 bytes. Endpoints without a secret receive no signature headers.

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

function verify(secret, headers, rawBody) {
  const key = secret.startsWith("whsec_")
    ? Buffer.from(secret.slice(6), "base64")
    : Buffer.from(secret, "utf8");
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // reject stale deliveries
  const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
  return headers["webhook-signature"]
    .split(" ")
    .some((sig) => {
      const value = sig.replace(/^v1,/, "");
      return value.length === expected.length &&
        crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
    });
}
```

Every delivery also carries `X-Webhook-Event` and `X-Webhook-Attempt`.

## Retries and disabling

For dashboard endpoints, delivery is tried three times in a row, 1 and 2 seconds apart. After that it is retried on a schedule of +5 min, +30 min, +2 h, +5 h, +8 h, +8 h and +10 h. A `429` reschedules the delivery without counting it as a failure. An endpoint that keeps failing for more than 12 hours is disabled until you re-enable it in **Settings → Webhooks**.

The full delivery schema, with every header and payload field, is in **Settings → API reference** in the app.
