---
title: Event catalogue
description: Every webhook event you can subscribe to and the payload shape it carries.
---

Each event uses the same envelope. The shape of `data` varies
by channel.

```json
{
  "id": "whe_…",
  "type": "<event.name>",
  "created_at": "2025-05-24T12:34:56.789Z",
  "data": { ... }
}
```

## Email events

### `email.sent`

Fires when the upstream network accepts the email for delivery.
This is the first signal that the message has left ZevSend.

### `email.delivered`

Fires when the recipient's mail server accepts the message.

### `email.bounced`

Permanent delivery failure — the address is invalid or the
inbox refused the message. The recipient is automatically added
to your team's suppression list.

```json
{
  "id": "whe_…",
  "type": "email.bounced",
  "created_at": "…",
  "data": {
    "message": {
      "public_id": "eml_…",
      "from_address": "noreply@acme.com",
      "to_address": "deleted@example.com",
      "subject": "Your receipt",
      "status": "bounced",
      "sandbox": false,
      "sent_at": "…"
    },
    "reason": "smtp; 550 5.1.1 No such user"
  }
}
```

### `email.complained`

Recipient marked the message as spam. The recipient is
automatically added to your team's suppression list.

### `email.failed`

Delivery permanently failed for a non-bounce reason — e.g. the
upstream network rejected the message before submitting.

## SMS events

### `sms.sent`

Fires when the network accepts the SMS for delivery.

### `sms.delivered`

Fires when the recipient's handset confirms receipt via a
network delivery report.

```json
{
  "id": "whe_…",
  "type": "sms.delivered",
  "created_at": "…",
  "data": {
    "message": {
      "public_id": "sms_…",
      "to_phone": "+2348012345678",
      "status": "delivered",
      "sandbox": false,
      "sent_at": "…"
    }
  }
}
```

### `sms.failed`

Synchronous rejection at submit time or a permanent failure
reported by a later delivery report. The `reason` field holds
the network's diagnostic where available.

## WhatsApp events

### `whatsapp.sent`

Fires when the network accepts the template send.

### `whatsapp.delivered`

Fires when the recipient's device confirms receipt.

### `whatsapp.read`

Fires when the recipient opens the conversation. This event
is best-effort — recipients with read receipts disabled never
trigger it.

```json
{
  "id": "whe_…",
  "type": "whatsapp.read",
  "created_at": "…",
  "data": {
    "message": {
      "public_id": "wam_…",
      "to_phone": "+2348012345678",
      "status": "read",
      "sandbox": false,
      "sent_at": "…"
    }
  }
}
```

### `whatsapp.failed`

Rejected at submit time or a permanent delivery failure. The
`reason` field surfaces the network's error message.

## Ordering and duplicates

We deliver events in order whenever the network behind them
provides ordering, but you should not depend on perfect
ordering. Two safe patterns:

- **Treat each event as a state assertion**, not a transition.
  An `email.delivered` for a message that's already at
  `bounced` in your store is a stale event and you should
  ignore it.
- **Use the `id` field as your dedupe key**. We retry on
  failure, so the same event may arrive more than once.