---
title: Event catalogue
description: Every webhook event, when it fires, and what its payload carries.
---

Twelve events in four groups. Subscribe only to the ones you act on:
an endpoint receiving everything and ignoring most of it is an
endpoint that falls behind.

## Audience

| Event | Fires when |
|---|---|
| `contact.subscribed` | Someone confirmed a subscription, or was subscribed through the API. On a double opt-in topic this fires on CONFIRMATION, not on the form submission, so it always means consent. |
| `contact.unsubscribed` | Someone opted out, from a link in an email or the preference centre. Treat it as authoritative and stop contacting them from your own systems too. |

## Delivery

| Event | Fires when |
|---|---|
| `email.delivered` | The recipient's mail provider accepted the message. |
| `email.bounced` | A permanent failure. The contact is suppressed automatically; you do not need to do it yourself. |
| `email.complained` | The recipient reported it. The contact is suppressed automatically. Worth alerting on: complaints are the fastest way to lose a sending reputation. |
| `email.failed` | We could not send to this recipient at all. Unlike a bounce, nothing left our side. |

## Engagement

| Event | Fires when |
|---|---|
| `email.opened` | A tracking pixel loaded. Indicative, not certain: images are blocked, prefetched and cached, so treat it as weak evidence and never as proof someone read it. |
| `email.clicked` | A tracked link was followed. Much stronger evidence of engagement than an open. |

## Campaigns

| Event | Fires when |
|---|---|
| `campaign.sending` | A campaign started dispatching. |
| `campaign.sent` | Every recipient of a campaign has been attempted. |
| `campaign.paused` | A campaign was paused mid-send. |
| `campaign.cancelled` | A campaign was cancelled mid-send. Queued recipients were not sent to and their quota was refunded. |
## Payload fields

Every per-recipient event (`email.*`) carries the same base:

| Field | Notes |
|---|---|
| `send_id` | This one recipient's send. Unique per recipient per campaign |
| `campaign_id` | |
| `campaign_name` | |
| `contact_id` | |
| `email` | |
| `occurred_at` | When it happened at the provider, not when we saw it |

Plus, per event:

**`email.bounced`**

| Field | Notes |
|---|---|
| `bounce_type` | `permanent`, `transient` or `undetermined` |
| `reason` | The provider's own wording. Useful, not parseable |
| `suppressed` | Whether we have stopped sending to this address |

Only a permanent bounce suppresses. A transient one means a full
mailbox or a bad afternoon, and the address is still mailable.

**`email.complained`**

| Field | Notes |
|---|---|
| `complaint_type` | The feedback loop's category, when given |
| `suppressed` | Always `true` |

**`email.failed`**

| Field | Notes |
|---|---|
| `reason` | Why it never reached the provider |

Distinct from a bounce: nothing left our side. A missing merge-tag
value, a render error, an address already suppressed. Do not treat
this as a dead address, or you will suppress perfectly good ones.

**`email.opened`**

| Field | Notes |
|---|---|
| `country` | Two-letter code, when we can tell. `null` otherwise |

**`email.clicked`**

| Field | Notes |
|---|---|
| `url` | The link followed |
| `country` | Two-letter code, or `null` |
| `device_type` | `mobile`, `tablet`, `desktop`, or `null` |
| `client` | Mail client, or `null` |

We report `null` rather than guessing. A confident wrong answer is
worse in a report than an honest gap.

**`contact.subscribed`**

| Field | Notes |
|---|---|
| `contact_id`, `email` | |
| `topic_id`, `topic_name` | |
| `brand_id` | |
| `source` | `double_opt_in`, `api`, `import` or `dashboard` |
| `occurred_at` | |

`source` tells you how strong the consent is. `double_opt_in` means
they clicked a link in their own inbox.

**`contact.unsubscribed`**

| Field | Notes |
|---|---|
| `contact_id`, `email` | |
| `scope` | `topic`, `brand` or `all` |
| `topic_id`, `topic_name` | Set when `scope` is `topic` |
| `brand_id` | Set for `topic` and `brand` |
| `campaign_id` | Which campaign the link was in, when it was one |
| `occurred_at` | |

**Read the `scope`.** `topic` means they left one topic, not your
whole workspace. Treating it as a global opt-out loses you a
subscriber who wanted to stay on everything else.

**`campaign.*`**

| Field | Notes |
|---|---|
| `campaign_id`, `campaign_name` | |
| `subject` | |
| `brand_id` | |
| `status` | |
| `recipient_count` | What the send was sized at |
| `occurred_at` | |

`campaign.sent` adds `delivered`, `bounced`, `failed` and `skipped`
as they stood when the send finished. Delivery keeps moving
afterwards, so treat them as a starting point and read the campaign
for current numbers.

## Which to subscribe to

**Keeping a CRM in sync:** `contact.subscribed`,
`contact.unsubscribed`. These change who you are allowed to contact
anywhere, including from your own transactional mail.

**List hygiene:** `email.bounced`, `email.complained`. We suppress
automatically on our side; subscribe if you want to mirror it in your
own database.

**Alerting:** `email.complained`. Complaints are the fastest way to
lose a sending reputation, and a sudden cluster is worth waking
somebody for.

**Engagement scoring:** `email.opened`, `email.clicked`. Weight
clicks far above opens.

**Operational dashboards:** `campaign.sent` tells you a send
finished without polling for it.