# Compliance events

Compliance events tell you when a recipient can't receive your email or doesn't want it: bounces, spam
complaints, unsubscribes, and messages the Email API refused or failed to queue. Subscribe to them to keep your
own records in step with the platform's decisions. A webhook for a compliance event can point to any URL.

Every example below is generated from the platform's code and is the exact body your URL receives. The values
are placeholders. The fields every event shares (`id`, `event`, `account_id`, `ip`, `user`, `created_on`) are
described in [the payload envelope](/guides/using-webhooks#the-payload-envelope). This page describes `data`.

The shape of `data` depends on how the email was sent:

- **Campaign**: a campaign.
- **Workflow**: an email sent by a workflow (the `/workflows` endpoints), including the confirmation email of a
  double opt-in list.
- **Transactional**: an email sent with the deprecated `POST /emails` endpoint.
- **Email API**: an email sent with `POST /v2/emails`. Automations send this way too, so their events have the
  Email API shape.

## Email.Bounced

Fires when a message bounces, whichever way it was sent.

### Campaign

```json snippet=webhook-events:Email.Bounced:campaign.json
{
  "id": "570bfeff-8a80-5f41-bb57-f2f343e49381",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "bounce": {
      "id": null,
      "dsn_message": "smtp; 550 5.1.1 <jane@example.com>: Recipient address rejected: User unknown",
      "source_ip": "192.0.2.10",
      "category": "bounce_hb"
    },
    "campaign": {
      "type": "campaign",
      "id": 4321,
      "name": "September newsletter"
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": null
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

### Workflow

```json snippet=webhook-events:Email.Bounced:automation-trigger.json
{
  "id": "ba5e6f52-aa7d-5c17-a238-29ca5a7fe955",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "bounce": {
      "id": null,
      "dsn_message": "smtp; 452 4.2.2 Mailbox full",
      "source_ip": "192.0.2.10",
      "category": "bounce_sb"
    },
    "automation": {
      "trigger": {
        "id": 2222,
        "name": "Welcome series step 1"
      }
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": null
    },
    "list": {
      "id": 111,
      "name": null
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

A double opt-in confirmation email names the system action instead of a trigger:

```json snippet=webhook-events:Email.Bounced:automation-system-action.json
{
  "id": "f0834bac-2a2b-59e6-9849-eb035d2607d2",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "bounce": {
      "id": null,
      "dsn_message": "smtp; 550 5.1.1 User unknown",
      "source_ip": "192.0.2.10",
      "category": "bounce_hb"
    },
    "automation": {
      "workflow": {
        "id": null,
        "name": null
      },
      "action": {
        "id": null,
        "name": null
      },
      "system_action": {
        "type": "douopt-in",
        "id": 3333,
        "name": "list_111_douopt_in"
      }
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": null
    },
    "list": {
      "id": 111,
      "name": null
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

### Transactional

```json snippet=webhook-events:Email.Bounced:legacy-transactional.json
{
  "id": "e4bb5dcb-bacc-524c-a881-3d3dcc2d07bb",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "id": 987657,
    "email": "jane@example.com",
    "sent_id": "445566",
    "sender_name": "Acme Billing",
    "sender_email": "billing@acme.example",
    "source_ip": "192.0.2.10",
    "envelope_from": "relay_1234.77.445566.123456789@bounce.example",
    "dsn_message": "smtp; 550 5.1.1 User unknown",
    "additional_headers": [
      {
        "name": "X-Order-Id",
        "value": "A-1001"
      }
    ],
    "contact": {
      "email": "jane@example.com"
    },
    "transactional_email": {
      "id": "445566",
      "sender": {
        "name": "Acme Billing",
        "email": "billing@acme.example"
      },
      "envelope_from": "relay_1234.77.445566.123456789@bounce.example",
      "additional_headers": [
        {
          "name": "X-Order-Id",
          "value": "A-1001"
        }
      ]
    },
    "bounce": {
      "id": 987657,
      "dsn_message": "smtp; 550 5.1.1 User unknown",
      "source_ip": "192.0.2.10",
      "category": "bounce_hb"
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "list": {
      "id": null,
      "name": null
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `bounce.id` | integer or null | The bounce record. Set for transactional emails; `null` for campaigns and workflows. |
| `bounce.category` | string | `bounce_hb` hard bounce, `bounce_sb` soft bounce, `bounce_fm` mailbox full, `bounce_mb` blocked, `bounce_tr` transient failure, `bounce_df` DNS failure, `bounce_ar` automatic reply, `bounce_cr` challenge-response, `bounce_ac` address change. |
| `bounce.dsn_message` | string | The receiving server's diagnostic. |
| `bounce.source_ip` | string | The platform address that sent the message. |
| `contact` | object | `id` (integer), `email`, and `status`, which is always `null` here. Transactional: `email` only. |
| `list` | object | Campaign: `id` and `name`. Workflow: `id`, with `name` always `null`. Transactional: both `null`. |
| `campaign` | object | Campaign only: `type` (`"campaign"`), `id`, `name`. |
| `automation` | object | Workflow only. `trigger` (`id`, `name`) for a workflow email. For an email that a list event sends, such as the double opt-in confirmation, `system_action`: `type` is that event, such as `douopt-in` or `opt-in`, plus `id` and `name`. `workflow` and `action` are then present but `null`. |
| `transactional_email` | object | Transactional only: `id` (a string), `sender` (`name`, `email`), `envelope_from`, and `additional_headers`, a list of `name`/`value` objects (or `null`). |
| `segment`, `context` | object | Always present, with `null` values. |
| `id`, `email`, `sent_id`, `sender_name`, `sender_email`, `source_ip`, `envelope_from`, `dsn_message`, `additional_headers` | | Transactional only: an older, flat copy of the same values. Read `bounce` and `transactional_email` instead. |

### Email API

```json snippet=webhook-events:Email.Bounced:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000001",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": "active",
      "subscribed_on": 1735689600,
      "custom_attributes": [
        {
          "name": "first_name",
          "value": "Jane"
        }
      ],
      "tags": [
        "vip"
      ],
      "interests": [
        "news"
      ]
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    }
  }
}
```

When the message wasn't sent to a list contact, `data` identifies the email only:

```json snippet=webhook-events:Email.Bounced:email-api-no-list.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000002",
  "event": "Email.Bounced",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `event_time` | integer | When the bounce was recorded, in Unix seconds. |
| `email.id` | string | The id that `POST /v2/emails` returned. |
| `contact` | object | Present when the message went to a list contact: `id`, `email`, `status`, `subscribed_on`, `custom_attributes`, `tags`, `interests`. |
| `list` | object | Present with `contact`: `id`, `name`. |

An Email API bounce doesn't say whether it was hard or soft, or why. To find out, [look the email
up](#enriching-email-api-events).

## Email.ReportedAsSpam

Fires when a recipient reports a message as spam to a mailbox provider that shares complaints with the platform.
For campaign, workflow and transactional messages, by the time you receive the event the contact's status is
already `spam` and the address is on the suppression list.

### Campaign

```json snippet=webhook-events:Email.ReportedAsSpam:campaign.json
{
  "id": "6c901b05-a966-53e2-aafe-e71f32a05aa9",
  "event": "Email.ReportedAsSpam",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "campaign": {
      "type": "campaign",
      "id": 4321,
      "name": "September newsletter"
    },
    "fbl": {
      "provider": "hotmail",
      "source_ip": "192.0.2.10"
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": "5678",
      "email": "jane@example.com",
      "status": "spam"
    },
    "list": {
      "id": "111",
      "name": "Newsletter subscribers"
    },
    "segment": {
      "id": 9,
      "name": "Canada",
      "query": "(`country` = \"CA\")"
    }
  }
}
```

### Workflow

```json snippet=webhook-events:Email.ReportedAsSpam:automation.json
{
  "id": "d221bed9-06c6-56f5-8074-ee3003cade41",
  "event": "Email.ReportedAsSpam",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "automation": {
      "trigger": {
        "id": 2222,
        "name": "Welcome series step 1"
      }
    },
    "fbl": {
      "provider": "yahoo",
      "source_ip": "192.0.2.10"
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": "5678",
      "email": "jane@example.com",
      "status": "spam"
    },
    "list": {
      "id": "111",
      "name": "Newsletter subscribers"
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

### Transactional

```json snippet=webhook-events:Email.ReportedAsSpam:legacy-transactional.json
{
  "id": "0e5183b6-36b3-58b1-ae43-52c86448359f",
  "event": "Email.ReportedAsSpam",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "contact": {
      "email": "jane@example.com"
    },
    "transactional_email": {
      "id": "445566",
      "sender": {
        "name": "Acme Billing",
        "email": "billing@acme.example"
      },
      "envelope_from": "relay_1234.77.445566.123456789@bounce.example",
      "additional_headers": [
        {
          "name": "X-Order-Id",
          "value": "A-1001"
        }
      ]
    },
    "fbl": {
      "provider": "aol",
      "source_ip": "192.0.2.10"
    },
    "context": {
      "timestamp": null,
      "user_agent": null,
      "ip": null
    },
    "list": {
      "id": null,
      "name": null
    },
    "segment": {
      "id": null,
      "name": null,
      "query": null
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `fbl.provider` | string | The mailbox provider that reported the complaint, such as `hotmail`, `yahoo` or `aol`. |
| `fbl.source_ip` | string | The platform address that sent the message. |
| `contact` | object | `id`, `email`, `status` (usually `spam`). **`id` is a string here.** Transactional: `email` only. |
| `list` | object | `id` and `name`. **`id` is a string here.** Transactional: both `null`. |
| `segment` | object | The targeted segment (`id`, `name`, `query`), or `null` values. |
| `campaign`, `automation`, `transactional_email` | object | As in [Email.Bounced](#emailbounced). |
| `context` | object | Always present, with `null` values. |

### Email API

```json snippet=webhook-events:Email.ReportedAsSpam:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000003",
  "event": "Email.ReportedAsSpam",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    }
  }
}
```

The Email API complaint identifies the email only: `event_time` and `email.id`. To find the recipient, [look
the email up](#enriching-email-api-events).

## Email.Unsubscribed

Fires only for the Email API, when a recipient unsubscribes from the list with the link or the
`List-Unsubscribe` header of an Email API message. An unsubscribe from a campaign or workflow email fires
[Contact.Removed](#contactremoved) instead.

```json snippet=webhook-events:Email.Unsubscribed:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000004",
  "event": "Email.Unsubscribed",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": "active",
      "subscribed_on": 1735689600,
      "custom_attributes": [
        {
          "name": "first_name",
          "value": "Jane"
        }
      ],
      "tags": [
        "vip"
      ],
      "interests": [
        "news"
      ]
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    },
    "context": {
      "ip": "203.0.113.7",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `event_time` | integer | When the recipient unsubscribed, in Unix seconds. |
| `email.id` | string | The id that `POST /v2/emails` returned. |
| `contact` | object | `id`, `email`, `status`, `subscribed_on`, `custom_attributes`, `tags`, `interests`. `status` is read while the unsubscribe is processed, so it can still be `active`. |
| `list` | object | The list the recipient left: `id`, `name`. Always present. |
| `context` | object | The recipient's `ip` and `user_agent`. |

## Email.GlobalUnsubscribed

Fires only for the Email API, when a recipient uses the link to unsubscribe from all email in an Email API
message.

```json snippet=webhook-events:Email.GlobalUnsubscribed:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000005",
  "event": "Email.GlobalUnsubscribed",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "context": {
      "ip": "203.0.113.7",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `event_time` | integer | When the recipient unsubscribed, in Unix seconds. |
| `email.id` | string | The id that `POST /v2/emails` returned. |
| `context` | object | The recipient's `ip` and `user_agent`. |

**The payload has no contact and no address.** The same click also fires
[SuppressedEmail.Added](#suppressedemailadded), which carries the address. You can also [look the email
up](#enriching-email-api-events). A global unsubscribe from a campaign or workflow email fires
SuppressedEmail.Added and Contact.Removed, not this event.

## Email.Rejected

Fires only for the Email API, when `POST /v2/emails` refuses a message before queueing it.

```json snippet=webhook-events:Email.Rejected:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000006",
  "event": "Email.Rejected",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": 1002260,
    "email": "dev@acme.example",
    "account_id": 1234
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": "unsubscribed",
      "subscribed_on": 1735689600,
      "custom_attributes": [
        {
          "name": "first_name",
          "value": "Jane"
        }
      ],
      "tags": [
        "vip"
      ],
      "interests": [
        "news"
      ]
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    },
    "reason": "unsubscribed"
  }
}
```

When the message wasn't sent to a list contact:

```json snippet=webhook-events:Email.Rejected:email-api-no-list.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000007",
  "event": "Email.Rejected",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": 1002260,
    "email": "dev@acme.example",
    "account_id": 1234
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "reason": "suppressed"
  }
}
```

| Field | Type | Description |
|---|---|---|
| `reason` | string | Why the message was refused: `bounced`, `unsubscribed`, `pending` (the contact hasn't confirmed), `spam`, `deleted` or `suppressed`. A message submitted with `content.type` set to `transactional` is never refused as `unsubscribed`. |
| `event_time` | integer | When the message was refused, in Unix seconds. |
| `email.id` | string | The id that `POST /v2/emails` returned. |
| `contact`, `list` | object | Present when the message went to a list contact, as in [Email.Bounced](#emailbounced). |

Unlike the other compliance events, `user` is set: it's the user who submitted the message.

## Email.Error

Fires only for the Email API, when a submitted message hits an internal error before it's queued. The message
isn't sent.

```json snippet=webhook-events:Email.Error:email-api.json
{
  "id": "7f3c9a2e-0000-4000-8000-000000000008",
  "event": "Email.Error",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": 1002260,
    "email": "dev@acme.example",
    "account_id": 1234
  },
  "created_on": 1790431393,
  "data": {
    "event_time": 1790431391,
    "email": {
      "id": "b1e2c3d4-0000-4000-8000-00000000abcd"
    },
    "error": "internal error; unable to queue"
  }
}
```

| Field | Type | Description |
|---|---|---|
| `error` | string | The error. It starts with `internal error;`. |
| `event_time` | integer | When the error happened, in Unix seconds. |
| `email.id` | string | The id that `POST /v2/emails` returned. |

`user` is the user who submitted the message.

## Contact.Removed

Fires when a contact leaves a list. That happens when the contact is unsubscribed (a link in a campaign or
workflow email, the API, a list unsubscribe or a global unsubscribe), or deleted
(`DELETE /lists/{list_id}/contacts/{contact_id}`). It doesn't fire when an import unsubscribes contacts.

### Unsubscribed through a link

```json snippet=webhook-events:Contact.Removed:unsubscribed.json
{
  "id": "4c1256b2-53f1-55b4-a33d-92e7b549b0bc",
  "event": "Contact.Removed",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "context": {
      "timestamp": 1790431391,
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)",
      "ip": "203.0.113.7"
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": "unsubscribed",
      "subscribed_on": 1735689600,
      "last_bounce_type": "none",
      "bounces_count": 0,
      "custom_attributes": [
        {
          "name": "first_name",
          "value": "Jane"
        }
      ]
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    }
  }
}
```

### Deleted by a user

```json snippet=webhook-events:Contact.Removed:deleted.json
{
  "id": "71e172f5-0f1a-5ea1-86cf-a8277dfe10ce",
  "event": "Contact.Removed",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": 1002260,
    "email": "dev@acme.example",
    "account_id": 1234
  },
  "created_on": 1790431393,
  "data": {
    "context": {
      "timestamp": 1790431391,
      "user_agent": null,
      "ip": null
    },
    "contact": {
      "id": 5678,
      "email": "jane@example.com",
      "status": "deleted",
      "subscribed_on": 1735689600,
      "last_bounce_type": "none",
      "bounces_count": 0,
      "custom_attributes": [
        {
          "name": "first_name",
          "value": "Jane"
        }
      ]
    },
    "list": {
      "id": 111,
      "name": "Newsletter subscribers"
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `list` | object | The list the contact left: `id`, `name`. |
| `contact` | object | `id`, `email`, `status` (`unsubscribed` or `deleted`), `subscribed_on`, `last_bounce_type`, `bounces_count`, `custom_attributes`. |
| `context` | object | `timestamp`: when it happened, in Unix seconds. `ip` and `user_agent` are the contact's when they unsubscribed through a link, and `null` otherwise. |

`user` is the user who unsubscribed or deleted the contact. Its values are `null` when the contact unsubscribed
themselves.

## SuppressedEmail.Added

Fires when an address is added to the suppression list. That happens when it's added with the API, and when a
recipient unsubscribes from all email, from a campaign, a workflow email or an Email API message. This is the
event to subscribe to if you need the address of a global unsubscribe.

```json snippet=webhook-events:SuppressedEmail.Added:global-unsubscribe.json
{
  "id": "3bd96742-0aa7-527a-b50a-9f260d74290d",
  "event": "SuppressedEmail.Added",
  "account_id": 1234,
  "account_lineage": null,
  "ip": "192.0.2.20",
  "user": {
    "id": null,
    "email": null,
    "account_id": null
  },
  "created_on": 1790431393,
  "data": {
    "suppressedEmail": {
      "email": "jane@example.com"
    }
  }
}
```

| Field | Type | Description |
|---|---|---|
| `suppressedEmail.email` | string | The suppressed address. Note the camel case in `suppressedEmail`. |

This page shows the global unsubscribe. Payloads for suppressed domains and local parts aren't documented yet.

## Enriching Email API events

Email API events identify the message with `data.email.id`. To get the recipient and the delivery details, call
[Retrieve a submitted email](/api/email-api#retrieve-a-submitted-email) with that id:

```bash
curl --request GET \
  --url 'https://api.cakemail.dev/v2/emails/b1e2c3d4-0000-4000-8000-00000000abcd' \
  --header 'Authorization: Bearer <access token>'
```

The endpoint allows one request per second per user and account, so make these lookups from a queue, not
inside your webhook handler.
