---
title: "Webhooks"
description: "Create and manage webhook endpoints for the organization, a pod or an inbox: set custom headers, send test events, list deliveries and replay failed ones."
canonical: "https://machinemail.dev/docs/reference/api/webhooks"
---

# Webhooks

> Create and manage webhook endpoints for the organization, a pod or an inbox: set custom headers, send test events, list deliveries and replay failed ones.

Source: https://machinemail.dev/docs/reference/api/webhooks

| Method   | Path                                                                           | Summary                                                             |
| -------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
| `GET`    | `/v0/inboxes/{inbox_id}/webhooks`                                              | [List Webhooks in Inbox](#list-webhooks-in-inbox)                   |
| `POST`   | `/v0/inboxes/{inbox_id}/webhooks`                                              | [Create Webhook in Inbox](#create-webhook-in-inbox)                 |
| `GET`    | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}`                                 | [Get Webhook in Inbox](#get-webhook-in-inbox)                       |
| `PATCH`  | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}`                                 | [Update Webhook in Inbox](#update-webhook-in-inbox)                 |
| `DELETE` | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}`                                 | [Delete Webhook in Inbox](#delete-webhook-in-inbox)                 |
| `GET`    | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}/deliveries`                      | [List Deliveries in Inbox](#list-deliveries-in-inbox)               |
| `POST`   | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay` | [Replay Delivery in Inbox](#replay-delivery-in-inbox)               |
| `GET`    | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}/headers`                         | [Get Webhook Headers in Inbox](#get-webhook-headers-in-inbox)       |
| `PATCH`  | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}/headers`                         | [Update Webhook Headers in Inbox](#update-webhook-headers-in-inbox) |
| `POST`   | `/v0/inboxes/{inbox_id}/webhooks/{webhook_id}/test`                            | [Send Test Event in Inbox](#send-test-event-in-inbox)               |
| `GET`    | `/v0/pods/{pod_id}/webhooks`                                                   | [List Webhooks in Pod](#list-webhooks-in-pod)                       |
| `POST`   | `/v0/pods/{pod_id}/webhooks`                                                   | [Create Webhook in Pod](#create-webhook-in-pod)                     |
| `GET`    | `/v0/pods/{pod_id}/webhooks/{webhook_id}`                                      | [Get Webhook in Pod](#get-webhook-in-pod)                           |
| `PATCH`  | `/v0/pods/{pod_id}/webhooks/{webhook_id}`                                      | [Update Webhook in Pod](#update-webhook-in-pod)                     |
| `DELETE` | `/v0/pods/{pod_id}/webhooks/{webhook_id}`                                      | [Delete Webhook in Pod](#delete-webhook-in-pod)                     |
| `GET`    | `/v0/pods/{pod_id}/webhooks/{webhook_id}/deliveries`                           | [List Deliveries in Pod](#list-deliveries-in-pod)                   |
| `POST`   | `/v0/pods/{pod_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay`      | [Replay Delivery in Pod](#replay-delivery-in-pod)                   |
| `GET`    | `/v0/pods/{pod_id}/webhooks/{webhook_id}/headers`                              | [Get Webhook Headers in Pod](#get-webhook-headers-in-pod)           |
| `PATCH`  | `/v0/pods/{pod_id}/webhooks/{webhook_id}/headers`                              | [Update Webhook Headers in Pod](#update-webhook-headers-in-pod)     |
| `POST`   | `/v0/pods/{pod_id}/webhooks/{webhook_id}/test`                                 | [Send Test Event in Pod](#send-test-event-in-pod)                   |
| `GET`    | `/v0/webhooks`                                                                 | [List Webhooks](#list-webhooks)                                     |
| `POST`   | `/v0/webhooks`                                                                 | [Create Webhook](#create-webhook)                                   |
| `GET`    | `/v0/webhooks/{webhook_id}`                                                    | [Get Webhook](#get-webhook)                                         |
| `PATCH`  | `/v0/webhooks/{webhook_id}`                                                    | [Update Webhook](#update-webhook)                                   |
| `DELETE` | `/v0/webhooks/{webhook_id}`                                                    | [Delete Webhook](#delete-webhook)                                   |
| `GET`    | `/v0/webhooks/{webhook_id}/deliveries`                                         | [List Deliveries](#list-deliveries)                                 |
| `POST`   | `/v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay`                    | [Replay Delivery](#replay-delivery)                                 |
| `GET`    | `/v0/webhooks/{webhook_id}/headers`                                            | [Get Webhook Headers](#get-webhook-headers)                         |
| `PATCH`  | `/v0/webhooks/{webhook_id}/headers`                                            | [Update Webhook Headers](#update-webhook-headers)                   |
| `POST`   | `/v0/webhooks/{webhook_id}/test`                                               | [Send Test Event](#send-test-event)                                 |

## List Webhooks in Inbox

```http
GET /v0/inboxes/{inbox_id}/webhooks
```

Same as List Webhooks (`GET /v0/webhooks`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |

#### Response

**200** A page of webhooks (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `webhooks` | `Webhook[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `webhooks` is a [Webhook](https://machinemail.dev/docs/reference/api/objects#webhook):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "webhooks": [
    {
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "url": "https://example.com/webhooks/machinemail",
      "event_types": ["message.received"],
      "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
      "inbox_ids": ["agent@machinemail.dev"],
      "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
      "inbox_id": "agent@machinemail.dev",
      "secret": "whsec_0123456789abcdef",
      "enabled": true,
      "updated_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "client_id": "my-client-id",
      "status": "active",
      "header_names": ["string"],
      "headers_updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Create Webhook in Inbox

```http
POST /v0/inboxes/{inbox_id}/webhooks
```

Same as Create Webhook (`POST /v0/webhooks`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_create`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `url` | `string` | required | Endpoint URL: a public http(s) URL (private and loopback addresses are rejected, and re-checked before every delivery). |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Event types the webhook receives. Omit it or send an empty array to receive every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `client_id` | `string` |  | Idempotency key: a create with a client_id already used in the organization returns that webhook with 200, or 409 when that webhook is outside the pod or inbox of the API key or route. |
| `inbox_ids` | `string[]` |  | Only deliver events of these inboxes. |
| `pod_ids` | `string[]` |  | Only deliver events of these pods. |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** A webhook with this client\_id already exists; it is returned unchanged. (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

**201** Created (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `409`  | Conflicts with an existing resource.                                                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/webhooks/machinemail",
  "event_types": [
    "message.received"
  ]
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook in Inbox

```http
GET /v0/inboxes/{inbox_id}/webhooks/{webhook_id}
```

Same as Get Webhook (`GET /v0/webhooks/{webhook_id}`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook in Inbox

```http
PATCH /v0/inboxes/{inbox_id}/webhooks/{webhook_id}
```

Same as Update Webhook (`PATCH /v0/webhooks/{webhook_id}`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Replaces the subscribed event types in full (not a merge). An empty array subscribes to every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. Omit it to keep the types. |
| `enabled` | `boolean` |  | false disables the webhook: no new deliveries are queued (deliveries already queued are still attempted). true enables it again. Events that occurred while it was disabled are not delivered later. |
| `add_inbox_ids` | `string[]` |  | Inboxes to add to inbox_ids. |
| `remove_inbox_ids` | `string[]` |  | Inboxes to remove from inbox_ids. |
| `add_pod_ids` | `string[]` |  | Pods to add to pod_ids. |
| `remove_pod_ids` | `string[]` |  | Pods to remove from pod_ids. |
| `url` | `string` |  | New endpoint URL: a public http(s) URL. |
| `headers` | `Record<string, string> \| null` |  | Replaces the custom delivery headers in full (not a merge): the webhook then sends exactly these. null or {} removes them all; omit it to keep them. To change one header and keep the others, use PATCH /v0/webhooks/{webhook_id}/headers. Values are write-only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** The updated webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event_types": [
    "message.received"
  ],
  "url": "https://example.com/webhooks/machinemail"
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Delete Webhook in Inbox

```http
DELETE /v0/inboxes/{inbox_id}/webhooks/{webhook_id}
```

Same as Delete Webhook (`DELETE /v0/webhooks/{webhook_id}`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_delete`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** Deleted (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `ok` | `true` | required |  |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X DELETE "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "ok": true
}
```

## List Deliveries in Inbox

```http
GET /v0/inboxes/{inbox_id}/webhooks/{webhook_id}/deliveries
```

Same as List Deliveries (`GET /v0/webhooks/{webhook_id}/deliveries`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |
| `status` | `"pending" \| "delivered" \| "failed"` |  | Only deliveries with this status. |

#### Response

**200** A page of deliveries (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `deliveries` | `WebhookDelivery[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `deliveries` is a [WebhookDelivery](https://machinemail.dev/docs/reference/api/objects#webhookdelivery):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "deliveries": [
    {
      "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
      "event_type": "message.received",
      "attempt": 1,
      "status": "pending",
      "response_status": 1,
      "response_body": "string",
      "next_attempt_at": "2026-10-01T12:00:00.000Z",
      "last_error": "string",
      "delivered_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Replay Delivery in Inbox

```http
POST /v0/inboxes/{inbox_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay
```

Same as Replay Delivery (`POST /v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |
| `delivery_id` | `string` | required | ID of the delivery to replay. |

#### Response

**200** The new delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries/dlv_01k6x3f1ns0000j5s1vz7d0m2p/replay" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook Headers in Inbox

```http
GET /v0/inboxes/{inbox_id}/webhooks/{webhook_id}/headers
```

Same as Get Webhook Headers (`GET /v0/webhooks/{webhook_id}/headers`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The header names (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `header_names` | `string[]` | required | Names of the custom delivery headers of this webhook, as they were set. Values are never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook Headers in Inbox

```http
PATCH /v0/inboxes/{inbox_id}/webhooks/{webhook_id}/headers
```

Same as Update Webhook Headers (`PATCH /v0/webhooks/{webhook_id}/headers`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

Sets, replaces or removes custom delivery headers; the headers you do not name are unchanged. Give at least one of `headers` (each name is added, or replaces the value of the header of that name in any case) or `remove_headers`. A name cannot be both set and removed in one request.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |
| `remove_headers` | `string[]` |  | Names of custom headers to remove (any case). Names that are not set are ignored. |

#### Response

**204** Updated

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## Send Test Event in Inbox

```http
POST /v0/inboxes/{inbox_id}/webhooks/{webhook_id}/test
```

Same as Send Test Event (`POST /v0/webhooks/{webhook_id}/test`), limited to the inbox.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `inbox_id` | `string` | required | ID of the inbox. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The test delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/inboxes/agent@machinemail.dev/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/test" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```

## List Webhooks in Pod

```http
GET /v0/pods/{pod_id}/webhooks
```

Same as List Webhooks (`GET /v0/webhooks`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |

#### Response

**200** A page of webhooks (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `webhooks` | `Webhook[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `webhooks` is a [Webhook](https://machinemail.dev/docs/reference/api/objects#webhook):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "webhooks": [
    {
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "url": "https://example.com/webhooks/machinemail",
      "event_types": ["message.received"],
      "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
      "inbox_ids": ["agent@machinemail.dev"],
      "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
      "inbox_id": "agent@machinemail.dev",
      "secret": "whsec_0123456789abcdef",
      "enabled": true,
      "updated_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "client_id": "my-client-id",
      "status": "active",
      "header_names": ["string"],
      "headers_updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Create Webhook in Pod

```http
POST /v0/pods/{pod_id}/webhooks
```

Same as Create Webhook (`POST /v0/webhooks`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_create`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `url` | `string` | required | Endpoint URL: a public http(s) URL (private and loopback addresses are rejected, and re-checked before every delivery). |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Event types the webhook receives. Omit it or send an empty array to receive every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `client_id` | `string` |  | Idempotency key: a create with a client_id already used in the organization returns that webhook with 200, or 409 when that webhook is outside the pod or inbox of the API key or route. |
| `inbox_ids` | `string[]` |  | Only deliver events of these inboxes. |
| `pod_ids` | `string[]` |  | Only deliver events of these pods. |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** A webhook with this client\_id already exists; it is returned unchanged. (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

**201** Created (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `409`  | Conflicts with an existing resource.                                                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/webhooks/machinemail",
  "event_types": [
    "message.received"
  ]
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook in Pod

```http
GET /v0/pods/{pod_id}/webhooks/{webhook_id}
```

Same as Get Webhook (`GET /v0/webhooks/{webhook_id}`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook in Pod

```http
PATCH /v0/pods/{pod_id}/webhooks/{webhook_id}
```

Same as Update Webhook (`PATCH /v0/webhooks/{webhook_id}`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Replaces the subscribed event types in full (not a merge). An empty array subscribes to every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. Omit it to keep the types. |
| `enabled` | `boolean` |  | false disables the webhook: no new deliveries are queued (deliveries already queued are still attempted). true enables it again. Events that occurred while it was disabled are not delivered later. |
| `add_inbox_ids` | `string[]` |  | Inboxes to add to inbox_ids. |
| `remove_inbox_ids` | `string[]` |  | Inboxes to remove from inbox_ids. |
| `add_pod_ids` | `string[]` |  | Pods to add to pod_ids. |
| `remove_pod_ids` | `string[]` |  | Pods to remove from pod_ids. |
| `url` | `string` |  | New endpoint URL: a public http(s) URL. |
| `headers` | `Record<string, string> \| null` |  | Replaces the custom delivery headers in full (not a merge): the webhook then sends exactly these. null or {} removes them all; omit it to keep them. To change one header and keep the others, use PATCH /v0/webhooks/{webhook_id}/headers. Values are write-only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** The updated webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event_types": [
    "message.received"
  ],
  "url": "https://example.com/webhooks/machinemail"
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Delete Webhook in Pod

```http
DELETE /v0/pods/{pod_id}/webhooks/{webhook_id}
```

Same as Delete Webhook (`DELETE /v0/webhooks/{webhook_id}`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_delete`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** Deleted (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `ok` | `true` | required |  |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X DELETE "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "ok": true
}
```

## List Deliveries in Pod

```http
GET /v0/pods/{pod_id}/webhooks/{webhook_id}/deliveries
```

Same as List Deliveries (`GET /v0/webhooks/{webhook_id}/deliveries`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |
| `status` | `"pending" \| "delivered" \| "failed"` |  | Only deliveries with this status. |

#### Response

**200** A page of deliveries (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `deliveries` | `WebhookDelivery[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `deliveries` is a [WebhookDelivery](https://machinemail.dev/docs/reference/api/objects#webhookdelivery):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "deliveries": [
    {
      "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
      "event_type": "message.received",
      "attempt": 1,
      "status": "pending",
      "response_status": 1,
      "response_body": "string",
      "next_attempt_at": "2026-10-01T12:00:00.000Z",
      "last_error": "string",
      "delivered_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Replay Delivery in Pod

```http
POST /v0/pods/{pod_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/replay
```

Same as Replay Delivery (`POST /v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |
| `delivery_id` | `string` | required | ID of the delivery to replay. |

#### Response

**200** The new delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries/dlv_01k6x3f1ns0000j5s1vz7d0m2p/replay" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook Headers in Pod

```http
GET /v0/pods/{pod_id}/webhooks/{webhook_id}/headers
```

Same as Get Webhook Headers (`GET /v0/webhooks/{webhook_id}/headers`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The header names (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `header_names` | `string[]` | required | Names of the custom delivery headers of this webhook, as they were set. Values are never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook Headers in Pod

```http
PATCH /v0/pods/{pod_id}/webhooks/{webhook_id}/headers
```

Same as Update Webhook Headers (`PATCH /v0/webhooks/{webhook_id}/headers`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

Sets, replaces or removes custom delivery headers; the headers you do not name are unchanged. Give at least one of `headers` (each name is added, or replaces the value of the header of that name in any case) or `remove_headers`. A name cannot be both set and removed in one request.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |
| `remove_headers` | `string[]` |  | Names of custom headers to remove (any case). Names that are not set are ignored. |

#### Response

**204** Updated

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## Send Test Event in Pod

```http
POST /v0/pods/{pod_id}/webhooks/{webhook_id}/test
```

Same as Send Test Event (`POST /v0/webhooks/{webhook_id}/test`), limited to the pod.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `pod_id` | `string` | required | ID of the pod. |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The test delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/pods/pod_01k6x2m9qd0001b8j4mr0w3d5f/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/test" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```

## List Webhooks

```http
GET /v0/webhooks
```

With an organization credential, every webhook of the organization, paginated. With a pod or inbox credential (or under a scoped path), the webhooks in that scope from the newest 500, in one page without `next_page_token`.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |

#### Response

**200** A page of webhooks (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `webhooks` | `Webhook[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `webhooks` is a [Webhook](https://machinemail.dev/docs/reference/api/objects#webhook):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "webhooks": [
    {
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "url": "https://example.com/webhooks/machinemail",
      "event_types": ["message.received"],
      "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
      "inbox_ids": ["agent@machinemail.dev"],
      "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
      "inbox_id": "agent@machinemail.dev",
      "secret": "whsec_0123456789abcdef",
      "enabled": true,
      "updated_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "client_id": "my-client-id",
      "status": "active",
      "header_names": ["string"],
      "headers_updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Create Webhook

```http
POST /v0/webhooks
```

The response includes `secret` (`whsec_...`), the key of the `X-MachineMail-Signature` HMAC. See the `webhooks` section of this document for the request MachineMail sends for each event type.

Under `/v0/pods/{pod_id}` or `/v0/inboxes/{inbox_id}`, or with a pod- or inbox-scoped credential, the webhook belongs to that pod or inbox (`pod_id`, `inbox_id`) and never receives events outside it.

A webhook delivers no event its creator could not read: each listed event type needs its read permission (`message.*` needs `message_read`, `inbox.*` `inbox_read`, `domain.*` `domain_read`, `draft.*` `draft_read`, `webhook.test` `webhook_read`), else `403`. An empty `event_types` from a credential without all of them is stored as the types it may read (`403` when there are none).

Custom request headers in `headers` are sent with every delivery (for example `Authorization: Bearer <token>` for an endpoint that requires it). Their values are write-only: responses list only `header_names`.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_create`.

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `url` | `string` | required | Endpoint URL: a public http(s) URL (private and loopback addresses are rejected, and re-checked before every delivery). |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Event types the webhook receives. Omit it or send an empty array to receive every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `client_id` | `string` |  | Idempotency key: a create with a client_id already used in the organization returns that webhook with 200, or 409 when that webhook is outside the pod or inbox of the API key or route. |
| `inbox_ids` | `string[]` |  | Only deliver events of these inboxes. |
| `pod_ids` | `string[]` |  | Only deliver events of these pods. |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** A webhook with this client\_id already exists; it is returned unchanged. (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

**201** Created (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `409`  | Conflicts with an existing resource.                                                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/webhooks" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/webhooks/machinemail",
  "event_types": [
    "message.received"
  ]
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook

```http
GET /v0/webhooks/{webhook_id}
```

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook

```http
PATCH /v0/webhooks/{webhook_id}
```

Fields you omit are unchanged. Webhooks are never disabled automatically after failed deliveries; `enabled: false` is the only way to disable one. New `event_types` follow the same permission rule as `POST /v0/webhooks`, and changing `url` needs the read permission of every event type the webhook delivers (`403` otherwise).

`headers` replaces the custom delivery headers in full, `null` or `{}` removes them, and omitting it keeps them. While a webhook has custom headers, changing `url` to another origin (scheme, host or port) needs `headers` in the same request (`400` otherwise), so header values never follow the webhook to a new server unless you send them again.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` |  | Replaces the subscribed event types in full (not a merge). An empty array subscribes to every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. Omit it to keep the types. |
| `enabled` | `boolean` |  | false disables the webhook: no new deliveries are queued (deliveries already queued are still attempted). true enables it again. Events that occurred while it was disabled are not delivered later. |
| `add_inbox_ids` | `string[]` |  | Inboxes to add to inbox_ids. |
| `remove_inbox_ids` | `string[]` |  | Inboxes to remove from inbox_ids. |
| `add_pod_ids` | `string[]` |  | Pods to add to pod_ids. |
| `remove_pod_ids` | `string[]` |  | Pods to remove from pod_ids. |
| `url` | `string` |  | New endpoint URL: a public http(s) URL. |
| `headers` | `Record<string, string> \| null` |  | Replaces the custom delivery headers in full (not a merge): the webhook then sends exactly these. null or {} removes them all; omit it to keep them. To change one header and keep the others, use PATCH /v0/webhooks/{webhook_id}/headers. Values are write-only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |

#### Response

**200** The updated webhook (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `url` | `string` | required | URL of the webhook endpoint. |
| `event_types` | `("message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released")[]` | required | Event types the webhook receives. Empty: every event type except domain.updated, message.released and trusted_domain.*, which are sent only when listed. |
| `pod_ids` | `string[]` |  | Only events of these pods. Absent: every pod in scope. |
| `inbox_ids` | `string[]` |  | Only events of these inboxes. Absent: every inbox in scope. |
| `pod_id` | `string` |  | Set when the webhook was created with a pod-scoped credential (or under /v0/pods/{pod_id}): it never receives events outside that pod. |
| `inbox_id` | `string` |  | Set when the webhook was created with an inbox-scoped credential (or under /v0/inboxes/{inbox_id}): it only receives events of that inbox. |
| `secret` | `string` | required | Signing secret (whsec_...) for X-MachineMail-Signature. Returned on every read. |
| `enabled` | `boolean` | required | Whether new events are delivered. Webhooks are never disabled automatically. |
| `updated_at` | `string (date-time)` | required | Time at which the webhook was last updated. |
| `created_at` | `string (date-time)` | required | Time at which the webhook was created. |
| `client_id` | `string` |  | Client ID given at creation (idempotency key). |
| `status` | `"active" \| "disabled"` | required | active or disabled; mirrors enabled. |
| `header_names` | `string[]` | required | Names of the custom delivery headers (headers), as they were set; empty when there are none. Values are write-only and never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "event_types": [
    "message.received"
  ],
  "url": "https://example.com/webhooks/machinemail"
}'
```

```json title="Response 200"
{
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "url": "https://example.com/webhooks/machinemail",
  "event_types": ["message.received"],
  "pod_ids": ["pod_01k6x2m9qd0001b8j4mr0w3d5f"],
  "inbox_ids": ["agent@machinemail.dev"],
  "pod_id": "pod_01k6x2m9qd0001b8j4mr0w3d5f",
  "inbox_id": "agent@machinemail.dev",
  "secret": "whsec_0123456789abcdef",
  "enabled": true,
  "updated_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "client_id": "my-client-id",
  "status": "active",
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Delete Webhook

```http
DELETE /v0/webhooks/{webhook_id}
```

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_delete`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** Deleted (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `ok` | `true` | required |  |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X DELETE "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "ok": true
}
```

## List Deliveries

```http
GET /v0/webhooks/{webhook_id}/deliveries
```

Delivery attempts of the webhook, one per event (a replay is a new delivery).

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Query parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `limit` | `integer` |  | Maximum number of items to return. Default 50, maximum 500; larger values are capped. |
| `page_token` | `string` |  | next_page_token from the previous page. |
| `ascending` | `boolean` |  | Sort oldest first. |
| `status` | `"pending" \| "delivered" \| "failed"` |  | Only deliveries with this status. |

#### Response

**200** A page of deliveries (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `count` | `integer` | required | Number of items returned. |
| `limit` | `integer` |  | Maximum number of items per page. Default 50, maximum 500; larger values are capped. |
| `next_page_token` | `string` |  | Page token for pagination. Absent on the last page. |
| `deliveries` | `WebhookDelivery[]` | required | Ordered by created_at, newest first unless ascending is set. |

Each item of `deliveries` is a [WebhookDelivery](https://machinemail.dev/docs/reference/api/objects#webhookdelivery):

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "count": 1,
  "limit": 50,
  "next_page_token": "eyJvIjo1MH0",
  "deliveries": [
    {
      "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
      "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
      "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
      "event_type": "message.received",
      "attempt": 1,
      "status": "pending",
      "response_status": 1,
      "response_body": "string",
      "next_attempt_at": "2026-10-01T12:00:00.000Z",
      "last_error": "string",
      "delivered_at": "2026-10-01T12:00:00.000Z",
      "created_at": "2026-10-01T12:00:00.000Z",
      "updated_at": "2026-10-01T12:00:00.000Z"
    }
  ]
}
```

## Replay Delivery

```http
POST /v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay
```

Queues a new delivery of the same event to the webhook and attempts it right away, even when the webhook is disabled. Returns the new delivery after that attempt.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |
| `delivery_id` | `string` | required | ID of the delivery to replay. |

#### Response

**200** The new delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/deliveries/dlv_01k6x3f1ns0000j5s1vz7d0m2p/replay" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Get Webhook Headers

```http
GET /v0/webhooks/{webhook_id}/headers
```

Names of the custom HTTP headers sent with every delivery to this webhook. Values are write-only and never returned.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_read`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The header names (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `header_names` | `string[]` | required | Names of the custom delivery headers of this webhook, as they were set. Values are never returned. |
| `headers_updated_at` | `string (date-time)` |  | When the custom headers were last set or changed. Absent when they never were. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "header_names": ["string"],
  "headers_updated_at": "2026-10-01T12:00:00.000Z"
}
```

## Update Webhook Headers

```http
PATCH /v0/webhooks/{webhook_id}/headers
```

Sets, replaces or removes custom delivery headers in one atomic change; headers you do not name keep their values. To replace the whole set (or clear it), use `headers` on `PATCH /v0/webhooks/{webhook_id}`. Values remain write-only.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Request body

Content type: `application/json`.

Sets, replaces or removes custom delivery headers; the headers you do not name are unchanged. Give at least one of `headers` (each name is added, or replaces the value of the header of that name in any case) or `remove_headers`. A name cannot be both set and removed in one request.

| Field | Type | | Description |
| --- | --- | --- | --- |
| `headers` | `Record<string, string>` |  | Custom HTTP headers sent with every delivery to this webhook (every attempt, retry, replay and test event), alongside the X-MachineMail-* headers, for example {"Authorization": "Bearer <token>"} for an endpoint that requires one. Values are write-only: they are stored encrypted and no response returns them; reads list the names (header_names). The headers are not part of the signed payload: X-MachineMail-Signature covers the body only. At most 10 headers. Names are HTTP tokens of up to 64 characters, compared without regard to case; values are printable ASCII (no line breaks or control characters) of up to 4096 characters, with surrounding spaces removed, and names plus values total at most 8192 bytes. Reserved names are refused with 400: Host, Content-Length, Content-Type, Content-Encoding, Transfer-Encoding, Connection, Upgrade, TE, Trailer, Keep-Alive, Expect, User-Agent, Accept-Encoding, and every name starting with Proxy-, Sec-, X-MachineMail-, Svix- or Webhook-. Authorization and Cookie are allowed. |
| `remove_headers` | `string[]` |  | Names of custom headers to remove (any case). Names that are not set are ignored. |

#### Response

**204** Updated

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Invalid request: error.type is validation and error.param names the field.                                                                                                                                                             |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X PATCH "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/headers" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## Send Test Event

```http
POST /v0/webhooks/{webhook_id}/test
```

Records a `webhook.test` event and delivers it to this webhook only (whatever its `event_types`, even when disabled), then returns the delivery after the first attempt.

**Authentication:** `Authorization: Bearer <api key>` or the dashboard `mm_session` cookie. Permission: `webhook_update`.

#### Path parameters

| Field | Type | | Description |
| --- | --- | --- | --- |
| `webhook_id` | `string` | required | ID of the webhook. |

#### Response

**200** The test delivery (`application/json`)

| Field | Type | | Description |
| --- | --- | --- | --- |
| `delivery_id` | `string` | required | ID of the delivery (X-MachineMail-Delivery-Id). |
| `webhook_id` | `string` | required | ID of the webhook. |
| `event_id` | `string` | required | ID of the event (X-MachineMail-Event-Id). |
| `event_type` | `"message.received" \| "message.received.spam" \| "message.received.quarantined" \| "message.received.unauthenticated" \| "message.received.blocked" \| "message.sent" \| "message.delivered" \| "message.bounced" \| "message.complained" \| "message.rejected" \| "domain.verified" \| "domain.updated" \| "inbox.created" \| "inbox.expired" \| "draft.sent" \| "webhook.test" \| "trusted_domain.created" \| "trusted_domain.updated" \| "trusted_domain.deleted" \| "message.released"` | required | Type of event. AgentMail has no message.received.quarantined, domain.updated, inbox.created, inbox.expired, draft.sent or webhook.test; MachineMail does not emit message.opened or calendar.event.*. domain.updated is sent whenever a DNS check changes the domain status or a record status (the change to VERIFIED also sends domain.verified). trusted_domain.created, .updated and .deleted (MachineMail) record changes to trusted domains. message.released (MachineMail) is sent for each held message a new trusted domain released (apply_to_existing), instead of message.received. Webhooks and WebSocket subscriptions receive domain.updated, message.released and the trusted_domain.* types only when their event_types list them, while the SSE stream and the event log always include them. |
| `attempt` | `integer` | required | Number of attempts so far. |
| `status` | `"pending" \| "delivered" \| "failed"` | required | pending until a 2xx response (delivered), or failed when retries stop (5 days after the event) or the webhook or event was deleted. |
| `response_status` | `integer` |  | HTTP status of the last attempt. |
| `response_body` | `string` |  | First 1024 bytes of the last response body. |
| `next_attempt_at` | `string (date-time)` |  | When the next attempt is due, while pending. |
| `last_error` | `string` |  | Error of the last failed attempt (HTTP 500, a network error). |
| `delivered_at` | `string (date-time)` |  | When the delivery succeeded. |
| `created_at` | `string (date-time)` | required | When the delivery was queued. |
| `updated_at` | `string (date-time)` | required | Last change to the delivery. |

#### Errors

| Status | Meaning                                                                                                                                                                                                                                |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | Missing, invalid, expired or revoked credential.                                                                                                                                                                                       |
| `403`  | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account\_suspended: error.message says what still works; do not retry or re-register). |
| `404`  | Not found. Also returned for resources outside the credential scope.                                                                                                                                                                   |
| `500`  | Unexpected server error.                                                                                                                                                                                                               |

Error bodies use the [error envelope](https://machinemail.dev/docs/reference/errors).

#### Example

```bash title="Request"
curl -X POST "https://api.machinemail.dev/v0/webhooks/whk_01k6x3c2jp0000f2p8rw4a7h9k/test" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
```

```json title="Response 200"
{
  "delivery_id": "dlv_01k6x3f1ns0000j5s1vz7d0m2p",
  "webhook_id": "whk_01k6x3c2jp0000f2p8rw4a7h9k",
  "event_id": "evt_01k6x3g4pt0000k6t2wa8e1n3q",
  "event_type": "message.received",
  "attempt": 1,
  "status": "pending",
  "response_status": 1,
  "response_body": "string",
  "next_attempt_at": "2026-10-01T12:00:00.000Z",
  "last_error": "string",
  "delivered_at": "2026-10-01T12:00:00.000Z",
  "created_at": "2026-10-01T12:00:00.000Z",
  "updated_at": "2026-10-01T12:00:00.000Z"
}
```
