## Event types

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:

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` | "event" | Always `event`. |
| `event_id` | string | Unique, time-sortable (`evt_..."). Use it to deduplicate. |
| `event_type` | string | One of the types above. |
| `timestamp` | string (date-time) | When the event was recorded. |
| `data` | object | The payload for this type (below). |
| payload keys |  | The keys of `data` are also copied to the top level, as AgentMail does (`message`, `thread`, `send`, `bounce`, …). |

The event log (`GET /v0/events`) returns a different shape for each event: `event_id`, `event_type`, `organization_id`, `pod_id`, `inbox_id`, `timestamp`, `data` and `created_at`.

## Payloads

The payload is what appears under `data` (and at the top level). [Message](/content/docs/reference/api/objects#message/index.html), [ThreadItem](/content/docs/reference/api/objects#threaditem/index.html), [Inbox](/content/docs/reference/api/objects#inbox/index.html) and [Domain](/content/docs/reference/api/objects#domain/index.html) objects are the same as the API returns elsewhere.

- Message content is untrusted input. When you pass mail to a model, use `message.trust.content.text` rather than `text` or `html`.
- `message.received*` payloads carry `message.authentication_results` (SPF, DKIM and DMARC verdicts with their domains), as AgentMail’s do. See [Authentication](/content/docs/trust/authentication#authentication_results-on-the-message/index.html).
- Domain events have no inbox, so webhooks and streams filtered by inbox never receive them.
- Expired inboxes are reaped once a minute, so `inbox.expired` can arrive up to about a minute after `expires_at`.

### domain.updated

A DNS check changed the domain `status` or a `records[].status` (verification progress). The change to `VERIFIED` also sends `domain.verified`. Sent only to webhooks whose `event_types` list it (an empty list does not include it).

FieldDescription

`domain` [Domain](/content/docs/reference/api/objects#domain/index.html) Required

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "domain.updated",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "domain": {
    "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
    "domain_id": "dom_01k6x3d5kq0000g3q9sx5b8j0m",
    "domain": "example.com",
    "...": "..."
  }
}
```

### domain.verified

A domain became `VERIFIED`.

FieldDescription

`domain` [Domain](/content/docs/reference/api/objects#domain/index.html) Required

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "domain.verified",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "domain": {
    "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
    "domain_id": "dom_01k6x3d5kq0000g3q9sx5b8j0m",
    "domain": "example.com",
    "...": "..."
  }
}
```

### draft.sent

A draft was sent, explicitly or at its `send_at`. A `message.sent` event is emitted too.

FieldDescription

`draft_id``string`Required

ID of the sent (and now deleted) draft.

`inbox_id``string`Required

ID of the inbox.

`send`objectRequired

`send`:

FieldDescription

`message_id``string`Required

ID of the message.

`thread_id``string`Required

ID of the thread.

`timestamp``string (date-time)`Required

When the message was sent.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "draft.sent",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "draft_id": "drf_01k6x3b7hn0000e1n7qv3z6g8j",
  "inbox_id": "agent@machinemail.dev",
  "send": {
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "timestamp": "2026-10-01T12:00:00.000Z"
  }
}
```

### inbox.created

An inbox was created (API, dashboard, catch-all, agent registration or an import).

FieldDescription

`inbox` [Inbox](/content/docs/reference/api/objects#inbox/index.html) Required

`import_source``"agentmail"`

Set when an importer copied the inbox from another provider (import_source on Create Inbox). Such events are recorded and streamed but never sent to webhooks.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "inbox.created",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "inbox": {
    "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
    "inbox_id": "agent@machinemail.dev",
    "email": "agent@machinemail.dev",
    "...": "..."
  },
  "import_source": "agentmail"
}
```

### inbox.expired

An ephemeral inbox expired and was deleted with its mail.

FieldDescription

`inbox` [Inbox](/content/docs/reference/api/objects#inbox/index.html) Required

`deleted_messages``integer`Required

Number of messages deleted with it.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "inbox.expired",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "inbox": {
    "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
    "inbox_id": "agent@machinemail.dev",
    "email": "agent@machinemail.dev",
    "...": "..."
  },
  "deleted_messages": 1
}
```

### message.bounced

A sent message bounced.

FieldDescription

`bounce` [Bounce](/content/docs/reference/api/objects#bounce/index.html) Required

`bounce` is a [Bounce](/content/docs/reference/api/objects#bounce/index.html):

FieldDescription

`inbox_id``string`Required

ID of the inbox.

`thread_id``string`Required

ID of the thread.

`message_id``string`Required

ID of the message.

`timestamp``string (date-time)`Required

When it happened.

`type``string`Required

Bounce type: Permanent or Transient (or the provider type).

`sub_type``string`Required

Bounce sub-type, General when the provider gives none.

`recipients` [Recipient[]](/content/docs/reference/api/objects#recipient/index.html) Required

Bounced recipients. Permanent bounces are added to the suppression list.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.bounced",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "bounce": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "timestamp": "2026-10-01T12:00:00.000Z",
    "type": "string",
    "sub_type": "string",
    "recipients": [
      {
        "address": "agent@machinemail.dev",
        "status": "string"
      }
    ]
  }
}
```

### message.complained

A recipient marked a sent message as spam.

FieldDescription

`complaint` [Complaint](/content/docs/reference/api/objects#complaint/index.html) Required

`complaint` is a [Complaint](/content/docs/reference/api/objects#complaint/index.html):

FieldDescription

`inbox_id``string`Required

ID of the inbox.

`thread_id``string`Required

ID of the thread.

`message_id``string`Required

ID of the message.

`timestamp``string (date-time)`Required

When it happened.

`type``string`Required

Complaint type, complaint when the provider gives none.

`sub_type``string`Required

Complaint sub-type, abuse when the provider gives none.

`recipients` [Recipient[]](/content/docs/reference/api/objects#recipient/index.html) Required

Complaining recipients, as {address, status} objects (AgentMail documents plain strings). They are added to the suppression list.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.complained",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "complaint": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "timestamp": "2026-10-01T12:00:00.000Z",
    "type": "string",
    "sub_type": "string",
    "recipients": [
      {
        "address": "agent@machinemail.dev",
        "status": "string"
      }
    ]
  }
}
```

### message.delivered

The receiving server accepted a sent message.

FieldDescription

`delivery` [Delivery](/content/docs/reference/api/objects#delivery/index.html) Required

`delivery` is a [Delivery](/content/docs/reference/api/objects#delivery/index.html):

FieldDescription

`inbox_id``string`Required

ID of the inbox.

`thread_id``string`Required

ID of the thread.

`message_id``string`Required

ID of the message.

`timestamp``string (date-time)`Required

When it happened.

`recipients``string[]`Required

Delivered recipients.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.delivered",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "delivery": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "timestamp": "2026-10-01T12:00:00.000Z",
    "recipients": ["agent@machinemail.dev"]
  }
}
```

### message.received, message.received.blocked, message.received.quarantined, message.received.spam, message.received.unauthenticated

A message was received and stored. The sub-types use the same payload.

FieldDescription

`message` [Message](/content/docs/reference/api/objects#message/index.html) Required

`thread` [ThreadItem](/content/docs/reference/api/objects#threaditem/index.html) Required

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "message": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "...": "..."
  },
  "thread": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "labels": ["UNREAD"],
    "...": "..."
  }
}
```

### message.rejected

An incoming message was refused and not stored.

FieldDescription

`reject` [Reject](/content/docs/reference/api/objects#reject/index.html) Required

`reject` is a [Reject](/content/docs/reference/api/objects#reject/index.html):

FieldDescription

`inbox_id``string`Required

ID of the inbox.

`thread_id``null`Required

Always null: nothing was stored.

`message_id``null`Required

Always null: nothing was stored.

`timestamp``string (date-time)`Required

When it happened.

`reason``"inbox_paused" | "plan_limit" | "plan_limit_expired"`Required

Why the message was rejected. inbox_paused: the inbox is paused. plan_limit (MachineMail): the organization reached a Free plan limit and already holds as much mail as it may. plan_limit_expired (MachineMail): held mail was not released in time and was deleted.

`from``string`Required

Sender of the rejected message.

`subject``string`

Subject of the rejected message.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.rejected",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "reject": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": null,
    "message_id": null,
    "timestamp": "2026-10-01T12:00:00.000Z",
    "reason": "inbox_paused",
    "from": "Support Agent <agent@machinemail.dev>",
    "subject": "Order #1042"
  }
}
```

### message.sent

A message was sent (send, reply, forward or draft).

FieldDescription

`send` [Send](/content/docs/reference/api/objects#send/index.html) Required

`message` [Message](/content/docs/reference/api/objects#message/index.html) Required

The sent message.

`thread` [ThreadItem](/content/docs/reference/api/objects#threaditem/index.html) Required

Its thread.

`send` is a [Send](/content/docs/reference/api/objects#send/index.html):

FieldDescription

`inbox_id``string`Required

ID of the inbox.

`thread_id``string`Required

ID of the thread.

`message_id``string`Required

ID of the message.

`timestamp``string (date-time)`Required

When it happened.

`recipients``string[]`Required

Every recipient (to, cc and bcc).

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.sent",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "send": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "timestamp": "2026-10-01T12:00:00.000Z",
    "recipients": ["agent@machinemail.dev"]
  },
  "message": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "message_id": "msg_01k6x3a1g00000d0m6pt2y5f7h",
    "...": "..."
  },
  "thread": {
    "inbox_id": "agent@machinemail.dev",
    "thread_id": "thd_01k6x3a1fz0000c9k5ns1x4e6g",
    "labels": ["UNREAD"],
    "...": "..."
  }
}
```

### trusted_domain.created

A trusted domain was created (MachineMail): the entry as it is now. Sent only to webhooks whose `event_types` list it (an empty list does not include it).

FieldDescription

`trusted_domain` [TrustedDomain](/content/docs/reference/api/objects#trusteddomain/index.html) Required

A trusted domain or address.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "trusted_domain.created",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "trusted_domain": {
    "trusted_domain_id": "string",
    "entry_type": "domain",
    "domain": "example.com",
    "...": "..."
  }
}
```

### trusted_domain.deleted

A trusted domain was deleted (MachineMail): the entry as it was. Sent only to webhooks whose `event_types` list it (an empty list does not include it).

FieldDescription

`trusted_domain` [TrustedDomain](/content/docs/reference/api/objects#trusteddomain/index.html) Required

A trusted domain or address.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "trusted_domain.deleted",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "trusted_domain": {
    "trusted_domain_id": "string",
    "entry_type": "domain",
    "domain": "example.com",
    "...": "..."
  }
}
```

### trusted_domain.updated

A trusted domain was updated (MachineMail): the entry as it is now. Sent only to webhooks whose `event_types` list it (an empty list does not include it).

FieldDescription

`trusted_domain` [TrustedDomain](/content/docs/reference/api/objects#trusteddomain/index.html) Required

A trusted domain or address.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "trusted_domain.updated",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "trusted_domain": {
    "trusted_domain_id": "string",
    "entry_type": "domain",
    "domain": "example.com",
    "...": "..."
  }
}
```

### webhook.test

Sent by `POST /v0/webhooks/{webhook_id}/test` to that webhook only, whatever its `event_types`.

FieldDescription

`webhook_id``string`Required

ID of the tested webhook.

`message``string`Required

This is a test event from MachineMail.

Example

```
{
  "type": "event",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "webhook.test",
  "timestamp": "2026-10-01T12:00:00.000Z",
  "data": {
    "...": "the same keys as at the top level"
  },
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "message": "string"
}
```

## Delivery

Each delivery is an HTTP `POST` with `Content-Type: application/json`, `User-Agent: MachineMail-Webhooks/1.0` and these headers:

FieldDescription

`X-MachineMail-Signature``string`Required

`t=<unix seconds>,v1=<hex HMAC-SHA256>`: the HMAC of `<t>.<raw request body>` with the webhook secret as key. Recompute it over the raw body, compare in constant time, and reject old t values (the reference verifier allows 300 seconds).

`X-MachineMail-Event``string`Required

Event type, as in event_type.

`X-MachineMail-Event-Id``string`Required

ID of the event, as in event_id. The same for every delivery and replay of the event: dedupe on it.

`X-MachineMail-Delivery-Id``string`Required

ID of this delivery (`GET /v0/webhooks/{webhook_id}/deliveries`). A replay is a new delivery.

- Respond with any `2xx` within **10 seconds**. Anything else counts as a failure: other status codes, timeouts, connection errors and redirects (redirects are not followed).
- The URL must be public `http` or `https`. It is checked again before every attempt, and private, loopback and link-local addresses are refused.
- The first 1024 bytes of your response body and its status are stored on the delivery (`response_status`, `response_body`).
- Delivery is at least once. A retry, a replay or a crash can deliver the same event twice; deduplicate on `X-MachineMail-Event-Id`.
- Deliveries are not ordered. Use `timestamp` or the event ID, which sorts by time, to order events.

## Retries

A failed attempt is retried on this schedule, measured from the previous attempt:

| After attempt | Next attempt in |
| --- | --- |
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 12 hours |
| 6 and later | 24 hours |

Retries stop when the next attempt would be more than **5 days** after the delivery was created; the delivery is then `failed`. That is about 10 attempts. The delivery worker checks for due retries every 5 seconds.

Webhooks are never disabled automatically. `PATCH /v0/webhooks/{webhook_id}` with `enabled: false` stops new deliveries; deliveries already queued are still attempted. Events that happen while a webhook is disabled are not delivered later.

## Inspecting and replaying

- `GET /v0/webhooks/{webhook_id}/deliveries?status=pending|delivered|failed` lists deliveries with `attempt`, `status`, `response_status`, `response_body`, `last_error`, `next_attempt_at` and `delivered_at`.
- `POST /v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay` sends the original event body again as a new delivery, right away, with a fresh signature timestamp. It works for any delivery status and whether or not the webhook is enabled. If it fails, the normal retry schedule applies.
- `POST /v0/webhooks/{webhook_id}/test` sends a `webhook.test` event to that webhook right away and returns the delivery.

Both need the `webhook_update` permission.

## Verifying signatures

Each webhook has a `secret` that starts with `whsec_`. It is returned when you create or fetch the webhook. There is no rotation endpoint: to rotate, create a new webhook and delete the old one.

The signature header is:

```
X-MachineMail-Signature: t=1759320000,v1=5f2b6c...e91a
```

`v1` is the lowercase hex HMAC-SHA256 of `<t>.<raw request body>`, keyed with the whole secret string (including `whsec_`, as UTF-8 bytes; the secret is not base64-decoded). `t` is the time of this attempt, so retries and replays carry new signatures.

To verify:

1. Read the raw body bytes before parsing JSON.
2. Split the header on `,` and each part on the first `=` to get `t` and `v1`.
3. Reject the request if `t` is more than 5 minutes from your clock.
4. Compute the HMAC and compare it with `v1` in constant time.

*TypeScriptPython*

```typescript
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyMachinemailSignature(secret: string, header: string | null, rawBody: string, toleranceSeconds = 300): boolean {
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(',').map((p) => {
      const i = p.indexOf('=');
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Example with a fetch-style handler (Bun, Deno, Hono, Next.js route handlers).
export async function handle(req: Request): Promise<Response> {
  const raw = await req.text();
  if (!verifyMachinemailSignature(process.env.MACHINEMAIL_WEBHOOK_SECRET!, req.headers.get('x-machinemail-signature'), raw)) {
    return new Response('invalid signature', { status: 401 });
  }
  const event = JSON.parse(raw);
  // Deduplicate on event.event_id, then hand off to a queue and return quickly.
  return new Response(null, { status: 204 });
}
```

```python
import hashlib
import hmac
import time

def verify_machinemail_signature(secret: str, header: str | None, raw_body: bytes, tolerance_seconds: int = 300) -> bool:
    if not header:
        return False
    parts = {}
    for part in header.split(","):
        key, _, value = part.partition("=")
        parts[key.strip()] = value.strip()
    try:
        t = int(parts["t"])
        v1 = parts["v1"]
    except (KeyError, ValueError):
        return False
    if abs(time.time() - t) > tolerance_seconds:
        return False
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

# Flask example
from flask import Flask, request, abort
import json, os

app = Flask(__name__)

@app.post("/webhooks/machinemail")
def machinemail_webhook():
    raw = request.get_data()
    if not verify_machinemail_signature(os.environ["MACHINEMAIL_WEBHOOK_SECRET"], request.headers.get("X-MachineMail-Signature"), raw):
        abort(401)
    event = json.loads(raw)
    # Deduplicate on event["event_id"], then process asynchronously.
    return "", 204
```

## Streams

The same envelopes are available without a public URL:

- **Server-sent events**: `GET /v0/events/stream`, filtered by `inbox_id`, `pod_id` and `event_types`. Resume after a disconnect with `Last-Event-ID` (up to 500 missed events are replayed). The stream starts with an `event: ready` frame and sends a `: ping` comment after 15 seconds without events.
- **WebSocket**: `/v0/ws`. Send `{"type":"subscribe","inbox_ids":[...],"pod_ids":[...],"event_types":[...]}` to narrow it, and `{"type":"ping"}` to get `{"type":"pong"}`. No resume.
- **Polling**: `GET /v0/events?after=<event_id>` returns everything after an event, oldest first.

Both streams accept the API key as `?token=` for clients that cannot set an `Authorization` header. `webhook.test` events are not streamed.
