---
title: "Webhook events"
description: "Every MachineMail webhook event type and its payload, the event envelope, delivery retries and replay, and how to verify the signature on each delivery."
canonical: "https://machinemail.dev/docs/reference/webhook-events"
---

# Webhook events

> Every MachineMail webhook event type and its payload, the event envelope, delivery retries and replay, and how to verify the signature on each delivery.

Source: https://machinemail.dev/docs/reference/webhook-events

MachineMail records an event when mail arrives, is sent, is delivered or bounces, and when inboxes, domains and drafts change. Each event is:

- POSTed to every matching [webhook](/content/docs/reference/api/webhooks/index.html),
- pushed to live streams ([`GET /v0/events/stream`](/content/docs/reference/api/events/index.html) and the `/v0/ws` WebSocket),
- kept in the event log ([`GET /v0/events`](/content/docs/reference/api/events/index.html)), with no retention limit.

## Event types

| Event type                         | When                                                                                                                                          |
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `message.received`                 | Mail arrived and the trust layer found nothing to flag.                                                                                       |
| `message.received.quarantined`     | Mail arrived with trust level `quarantined` (prompt injection, phishing, or a blocked sender). Its `INBOX` label is replaced by `QUARANTINE`. |
| `message.received.spam`            | Mail arrived that the classifier scored as spam (`is_spam` at least 0.9). `INBOX` is replaced by `SPAM`.                                      |
| `message.received.unauthenticated` | Mail arrived that failed DMARC, or failed both SPF and DKIM. `UNAUTHENTICATED` is added; `INBOX` stays.                                       |
| `message.received.blocked`         | Mail arrived from a sender on a receive block list. Its only label is `BLOCKED`.                                                              |
| `message.sent`                     | A message was sent: send, reply, reply-all, forward or draft send.                                                                            |
| `message.delivered`                | The receiving server accepted a sent message.                                                                                                 |
| `message.bounced`                  | A sent message bounced. Permanent bounces add the recipients to [suppressions](/content/docs/reference/api/suppressions/index.html).      |
| `message.complained`               | A recipient marked a sent message as spam. The recipient is suppressed.                                                                       |
| `message.rejected`                 | Mail arrived for a paused inbox and was not stored.                                                                                           |
| `domain.verified`                  | A custom domain passed DNS verification.                                                                                                      |
| `domain.updated`                   | A DNS check changed a custom domain's `status` or a record's `status`. Becoming `VERIFIED` also fires `domain.verified`.                      |
| `inbox.created`                    | An inbox was created, through the API, MCP, the dashboard, or a catch-all domain.                                                             |
| `inbox.expired`                    | An ephemeral inbox reached `expires_at` and was deleted with its mail.                                                                        |
| `draft.sent`                       | A draft was sent, by request or on its `send_at` schedule.                                                                                    |
| `webhook.test`                     | Sent only by `POST /v0/webhooks/{webhook_id}/test`, to that one webhook.                                                                      |

An inbound message fires exactly one `message.received*` event. The checks run in this order and the first match wins: blocked sender, quarantined, spam, unauthenticated, otherwise `message.received`.

When you create a webhook, `event_types` picks the types it receives. Omitting it, or sending an empty array, subscribes it to every type. An unknown type returns `400 Unknown event type: <type>`.

MachineMail does not emit AgentMail's `message.opened` or calendar events.

## Envelope

Every webhook body, SSE `data:` line and WebSocket event frame is the same JSON envelope:

```json title="Envelope"
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "message": { "...": "the payload keys, at the top level" },
  "thread": { "...": "..." },
  "data": {
    "message": { "...": "the same payload, again under data" },
    "thread": { "...": "..." }
  }
}
```

| Field        | Type               | Meaning                                                                                                              |
| ------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `type`       | `
