webhook events.md
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,
- pushed to live streams (
GET /v0/events/streamand the/v0/wsWebSocket), - kept in the event log (
GET /v0/events), 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. |
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:
{
"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 |
` |