Webhooks
Event catalogue
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.
Updated at, Saturday, October 10, 2026