---
title: "Webhooks"
description: "Every operation in this group: what it accepts, what it returns and the errors it can answer with."
url: "https://openemail.uk/docs/api/reference/webhooks"
area: "API"
category: "Reference"
---

# Webhooks

Every operation in this group: what it accepts, what it returns and the errors it can answer with.

## Operations

Where to call when mail arrives or goes out.

### `GET /webhooks`

List webhook endpoints

A page of endpoints, newest first. Follow `nextCursor` while `hasMore` is true to read them all.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Query parameters**

- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200`: A page of endpoints. Secrets are never echoed.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.list()`](https://openemail.uk/docs/sdk/reference/webhooks#list), [`webhooks.listAll()`](https://openemail.uk/docs/sdk/reference/webhooks#listAll), [`webhooks.iterate()`](https://openemail.uk/docs/sdk/reference/webhooks#iterate); CLI [`openemail webhooks list`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list); MCP [`getWebhook`](https://openemail.uk/docs/mcp/tools/keys#getWebhook), [`listWebhooks`](https://openemail.uk/docs/mcp/tools/keys#listWebhooks).

### `POST /webhooks`

Register a webhook endpoint

Returns the signing secret ONCE. https only, and private or loopback hosts are refused. A webhook is a server-side fetch to an address you supply, which is the shape of an SSRF.

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.
- Asks an OAuth access token for a verification code.

**Request body**

- `url` (`string`, required, format `uri`): The https receiver URL. Another scheme or a blocked host is 422 `invalid_webhook_url`. Stored in normalised form, so the `url` read back can differ cosmetically.
- `eventTypes` (`string[]`, up to 24 items, one of `"email.received"`, `"email.replied"`, `"email.sent"`, `"email.failed"`, `"email.cancelled"`, `"email.scheduled"`, `"email.queued"`, `"email.delivered"`, `"email.delivery_delayed"`, `"email.bounced"`, `"email.complained"`, `"email.suppressed"`, `"email.opened"`, `"email.clicked"`, `"email.downloaded"`, `"domain.verified"`, `"domain.sending_changed"`, `"domain.deleted"`, `"suppression.added"`, `"suppression.removed"`, `"file.uploaded"`, `"file.deleted"`, `"form.submitted"`, `"form.confirmed"`): Empty means the default set: the email events. Events outside it, such as email.replied, domain.*, suppression.*, file.* and form.*, have to be named. A webhook limited to particular addresses or domains never receives form.* events, because sign-ups belong to the whole workspace.
- `description` (`string`, up to 200 characters): Free text note, at most 200 characters.
- `addressAllowlist` (`string[]`, up to 50 items): Single addresses this endpoint hears about. Empty on both lists means every address this workspace owns.
- `domainAllowlist` (`string[]`, up to 25 items): Whole domains this endpoint hears about, including addresses added to them later.

**Returns**

- `201`: Created, with the secret.

**Errors**

- `403`: The key lacks the scope, or may not send as that address. `step_up_required`: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with `POST /security/step-up`, send it to `POST /security/step-up/verify`, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.
- The errors every operation can return: `400`, `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.create()`](https://openemail.uk/docs/sdk/reference/webhooks#create); CLI [`openemail webhooks create`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-create); MCP [`createWebhook`](https://openemail.uk/docs/mcp/tools/keys#createWebhook).

### `GET /webhooks/deliveries`

List deliveries across endpoints

The delivery log of every endpoint in the workspace, or of the ones `endpointIds` names, newest first and a page at a time: the all-endpoints Deliveries tab of the app. Each row carries `endpointId`. Nothing is pruned, so following `nextCursor` while `hasMore` is true reaches the first delivery. `status`, `since` and `until` narrow it exactly as they narrow one endpoint's log. The body that was sent is not here; read it with `GET /webhooks/{id}/deliveries/{deliveryId}`.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Query parameters**

- `endpointIds` (`string`): Comma-separated endpoint ids, at most 50. Left out, every endpoint in the workspace.
- `status` (`string`, one of `"delivered"`, `"failed"`): `failed` for the attempts that did not get a 2xx, the "only failed" view of the Deliveries tab, or `delivered` for the ones that did.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The `nextCursor` of the previous page, which is a delivery id. Keyset, not offset, and it holds under every filter: send the same filters with each page. A cursor that names no delivery in this list is a 400 `invalid_cursor`.

**Returns**

- `200` `WebhookDeliveryList`: A page of deliveries, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.listWorkspaceDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#listWorkspaceDeliveries), [`webhooks.listAllWorkspaceDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#listAllWorkspaceDeliveries), [`webhooks.iterateWorkspaceDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#iterateWorkspaceDeliveries); CLI [`openemail webhooks list-workspace-deliveries`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list-workspace-deliveries); MCP [`listWebhookDeliveries`](https://openemail.uk/docs/mcp/tools/keys#listWebhookDeliveries).

### `GET /webhooks/activity`

List webhook activity

The audit log of the workspace's webhooks, newest first: created, updated, enabled, disabled, auto_disabled, secret_rotated, tested, replayed and removed, whether the change came from the app, from a key over this API, or from OpenEmail itself. `actor` says who, as `@username` for a person or `API key <name>` for a key. Nothing is pruned, and a removed endpoint keeps its history. `endpointIds`, `since` and `until` narrow it.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Query parameters**

- `endpointIds` (`string`): Comma-separated endpoint ids, at most 50. Left out, every endpoint in the workspace.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `WebhookActivityList`: A page of changes, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.listWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#listWorkspaceActivity), [`webhooks.listAllWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#listAllWorkspaceActivity), [`webhooks.iterateWorkspaceActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#iterateWorkspaceActivity); CLI [`openemail webhooks list-workspace-activity`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list-workspace-activity); MCP [`listWebhookActivity`](https://openemail.uk/docs/mcp/tools/keys#listWebhookActivity).

### `GET /webhooks/{id}`

Retrieve a webhook endpoint

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Returns**

- `200`: The endpoint.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.get()`](https://openemail.uk/docs/sdk/reference/webhooks#get); CLI [`openemail webhooks get`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-get); MCP [`getWebhook`](https://openemail.uk/docs/mcp/tools/keys#getWebhook), [`listWebhooks`](https://openemail.uk/docs/mcp/tools/keys#listWebhooks).

### `PATCH /webhooks/{id}`

Update a webhook endpoint

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.
- Asks an OAuth access token for a verification code.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Request body**

- `url` (`string`, format `uri`): Replacement https URL, checked against the same host rules as `create`.
- `eventTypes` (`string[]`, up to 24 items, one of `"email.received"`, `"email.replied"`, `"email.sent"`, `"email.failed"`, `"email.cancelled"`, `"email.scheduled"`, `"email.queued"`, `"email.delivered"`, `"email.delivery_delayed"`, `"email.bounced"`, `"email.complained"`, `"email.suppressed"`, `"email.opened"`, `"email.clicked"`, `"email.downloaded"`, `"domain.verified"`, `"domain.sending_changed"`, `"domain.deleted"`, `"suppression.added"`, `"suppression.removed"`, `"file.uploaded"`, `"file.deleted"`, `"form.submitted"`, `"form.confirmed"`): Complete replacement subscription set. `[]` means the default `email.*` set.
- `description` (`string`, nullable, up to 200 characters): Replacement note of at most 200 characters, or null to clear it.
- `enabled` (`boolean`): False stops deliveries. True resumes them and resets `consecutiveFailures`.
- `addressAllowlist` (`string[]`, up to 50 items): Single addresses this endpoint hears about. Empty on both lists means every address this workspace owns.
- `domainAllowlist` (`string[]`, up to 25 items): Whole domains this endpoint hears about, including addresses added to them later.

**Returns**

- `200`: Saved.

**Errors**

- `403`: The key lacks the scope, or may not send as that address. `step_up_required`: the call was made with an OAuth access token that has not been verified in the last 60 minutes. Ask for a code with `POST /security/step-up`, send it to `POST /security/step-up/verify`, then repeat the call. The person can also choose Allow changes for 60 minutes on the app in Account settings, Connected apps, on the OpenEmail website. An API key is never asked for a code.
- The errors every operation can return: `400`, `401`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.update()`](https://openemail.uk/docs/sdk/reference/webhooks#update); CLI [`openemail webhooks update`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-update); MCP [`updateWebhook`](https://openemail.uk/docs/mcp/tools/keys#updateWebhook).

### `DELETE /webhooks/{id}`

Delete a webhook endpoint

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Returns**

- `200`: Deleted.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.delete()`](https://openemail.uk/docs/sdk/reference/webhooks#delete); CLI [`openemail webhooks delete`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-delete); MCP [`deleteWebhook`](https://openemail.uk/docs/mcp/tools/keys#deleteWebhook).

### `POST /webhooks/{id}/rotate-secret`

Rotate the signing secret

Returns the new secret once. The old one stops working immediately.

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Returns**

- `200`: Rotated.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.rotateSecret()`](https://openemail.uk/docs/sdk/reference/webhooks#rotateSecret); CLI [`openemail webhooks rotate-secret`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-rotate-secret); MCP [`rotateWebhookSecret`](https://openemail.uk/docs/mcp/tools/keys#rotateWebhookSecret).

### `POST /webhooks/{id}/test`

Send a synthetic event

Proves an endpoint is reachable and its signature check correct before any real mail depends on it. Returns what came back. A 404 from your server is the useful answer.

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Returns**

- `200`: The delivery result.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.test()`](https://openemail.uk/docs/sdk/reference/webhooks#test); CLI [`openemail webhooks test`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-test); MCP [`testWebhook`](https://openemail.uk/docs/mcp/tools/keys#testWebhook).

### `GET /webhooks/{id}/deliveries`

List deliveries

Every attempt, newest first and a page at a time, each with its response code, duration and attempt number. Nothing is dropped from the log, so following `nextCursor` while `hasMore` is true reaches the endpoint's first delivery. One event can appear several times: a delivery is tried up to 8 times, as it happens and then after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours, about 27 and a half hours in all, and a replay adds another row. `eventId` is the event, the same on every retry and replay of it, while `attempt` is the try, so a receiver reading this can tell a repeat from a new event. `nextAttemptAt` is when the automatic retry that follows an attempt is due, and null once none is waiting. `status` keeps only failed or only delivered attempts, the "only failed" view of the app, and `since` and `until` keep a window; the cursor stays valid under every filter as long as each page sends the same ones. `GET /webhooks/deliveries` reads every endpoint at once.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Query parameters**

- `status` (`string`, one of `"delivered"`, `"failed"`): `failed` for the attempts that did not get a 2xx, the "only failed" view of the Deliveries tab, or `delivered` for the ones that did.
- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The `nextCursor` of the previous page, which is a delivery id. Keyset, not offset, and it holds under every filter: send the same filters with each page. A cursor that names no delivery in this list is a 400 `invalid_cursor`.

**Returns**

- `200` `WebhookDeliveryList`: A page of deliveries, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.listDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#listDeliveries), [`webhooks.listAllDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#listAllDeliveries), [`webhooks.iterateDeliveries()`](https://openemail.uk/docs/sdk/reference/webhooks#iterateDeliveries); CLI [`openemail webhooks list-deliveries`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list-deliveries); MCP [`listWebhookDeliveries`](https://openemail.uk/docs/mcp/tools/keys#listWebhookDeliveries).

### `GET /webhooks/{id}/deliveries/{deliveryId}`

Retrieve a delivery

One attempt in full: `payload` is the exact JSON body that was POSTed, `responseBody` the first 2,000 characters your server answered, `attempts` every try of the same event on this endpoint, oldest first, and `nextAttemptAt` when the next automatic retry of the event is due. `replayRefusal` is null when a replay would be accepted, and otherwise carries the `code` and `message` the replay operation would answer with, one of `webhook_disabled`, `event_not_subscribed`, `event_out_of_scope`, `delivery_not_replayable` or `retry_in_progress`, the last while an automatic retry of the same event is being sent. A delivery that belongs to another endpoint is a 404, the same as one that never existed. A key narrowed to particular addresses or domains may read a delivery only on an endpoint whose own allowlists sit inside what the key holds, where holding one address never covers its whole domain, because the body names the addresses the event is about; any other is 422 `capability_unsupported` on `addressAllowlist`. Over OAuth only the workspace owner, or a member whose role reaches every address, may read one, and any other member's token is refused with `owner_only`. That member's token is held to the domains the workspace has now, so it reads only a delivery of an endpoint with allowlists, and one with none is 422 `capability_unsupported` for it too. In the app that member reads the deliveries of every endpoint, and so does an MCP client they connected with every address.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.
- `deliveryId` (`string`, required): A delivery id, `whd_` followed by 24 hex characters.

**Returns**

- `200`: The delivery.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.getDelivery()`](https://openemail.uk/docs/sdk/reference/webhooks#getDelivery); CLI [`openemail webhooks get-delivery`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-get-delivery); MCP [`getWebhookDelivery`](https://openemail.uk/docs/mcp/tools/keys#getWebhookDelivery).

### `POST /webhooks/{id}/deliveries/{deliveryId}/replay`

Replay a delivery

Sends the stored event again, once, right now, and answers with how it went. The body carries the same `id`, `type`, `createdAt` and `data` as the original, so a receiver that drops ids it has already handled treats it as the same event. Only the signature is new, because every POST is signed at the moment it is sent. It works on a delivered attempt as well as a failed one, which is how a receiver that lost its own copy is brought back in step. The replay is recorded as a new delivery, attempt 1 of 1, and is never retried automatically. Before it is sent, the automatic retries of the same event that have not started are paused: when the replay is delivered they stay cancelled, and when it fails they resume on their schedule. It answers 200 whatever your server said, so read `delivery.status`. Refused with a 409 when the endpoint is switched off (`webhook_disabled`), no longer listens for the event (`event_not_subscribed`), no longer covers the address the event is about (`event_out_of_scope`), the attempt has no stored event to send (`delivery_not_replayable`), an automatic retry of the same event is being sent at that moment (`retry_in_progress`), or another replay of the same event is still being sent (`replay_in_progress`). In the last two cases nothing is sent, so two copies never go out at once, even when two replays arrive at the same instant. Replay is one event at a time; there is no operation that sends every failed delivery again.

Requires the `webhooks:write` scope.

- Scopes: `webhooks:write`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.
- `deliveryId` (`string`, required): Any attempt of the event to send again, `whd_` followed by 24 hex characters.

**Returns**

- `200`: The replay and what your server answered.

**Errors**

- `409`: `webhook_disabled`, `event_not_subscribed`, `event_out_of_scope`, `delivery_not_replayable`, `retry_in_progress` or `replay_in_progress`.
- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.replayDelivery()`](https://openemail.uk/docs/sdk/reference/webhooks#replayDelivery); CLI [`openemail webhooks replay-delivery`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-replay-delivery); MCP [`replayWebhookDelivery`](https://openemail.uk/docs/mcp/tools/keys#replayWebhookDelivery).

### `GET /webhooks/{id}/activity`

List one webhook's activity

The audit log of one endpoint, newest first, the Activity tab of the endpoint in the app. It answers for a removed endpoint too, because its history is kept; an id with neither an endpoint nor any history in this workspace is a 404.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Path parameters**

- `id` (`string`, required): Endpoint id, `whe_` followed by 24 hex characters.

**Query parameters**

- `since` (`string`, format `date-time`): Only rows at or after this instant, ISO 8601. One that does not parse is a 400 `invalid_parameter`.
- `until` (`string`, format `date-time`): Only rows before this instant. It has to be later than `since`.
- `limit` (`integer`, at least 1, at most 100, default `25`): Rows per page, 1 to 100.
- `cursor` (`string`): The previous page's `nextCursor`, passed back as it came. It is opaque: it holds where the last row sat in this list's order, so a row deleted or edited between pages never breaks the walk, and the next page starts at the first row that sorts after it. A value this list did not hand out is a 400 `invalid_cursor`.

**Returns**

- `200` `WebhookActivityList`: A page of changes, newest first.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.listActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#listActivity), [`webhooks.listAllActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#listAllActivity), [`webhooks.iterateActivity()`](https://openemail.uk/docs/sdk/reference/webhooks#iterateActivity); CLI [`openemail webhooks list-activity`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list-activity); MCP [`listWebhookActivity`](https://openemail.uk/docs/mcp/tools/keys#listWebhookActivity).

### `GET /webhooks/events`

List webhook events

Every event an endpoint can subscribe to, with a line saying when it fires, and the limits an endpoint is held to: how many endpoints this workspace may have, and how many addresses and domains one allowlist may name. An endpoint that names no events receives every email event except `email.replied`.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Returns**

- `200` `WebhookCatalogue`: The events and the limits.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.listEvents()`](https://openemail.uk/docs/sdk/reference/webhooks#listEvents); CLI [`openemail webhooks list-events`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-list-events); MCP [`listWebhookEvents`](https://openemail.uk/docs/mcp/tools/keys#listWebhookEvents).

### `GET /webhooks/stats`

Read webhook delivery stats

How the deliveries of every endpoint, or of the ones `endpointIds` names, went inside a window: attempts, delivered and failed, the median time a receiver took to answer, a series of buckets, the events sent and the response codes received. It is the Analytics tab of the Webhooks page. Each try of an event counts as one attempt.

Requires the `webhooks:read` scope.

- Scopes: `webhooks:read`.

**Query parameters**

- `endpointIds` (`string`): Comma-separated endpoint ids, at most 50. Left out, every endpoint you can see.
- `since` (`string`, format `date-time`): The start of the window, an ISO 8601 instant. Left out, 30 days before `until`.
- `until` (`string`, format `date-time`): The end of the window, an ISO 8601 instant, not included. Left out, now.
- `grain` (`string`, one of `"minute"`, `"hour"`, `"day"`, default `"day"`): How wide one bucket of the series is. The bucket keys change shape with it: `YYYY-MM-DD` for a day, `YYYY-MM-DDTHH` for an hour, `YYYY-MM-DDTHH:MM` for a minute.
- `offsetMinutes` (`integer`, at least -840, at most 840, default `0`): Minutes to add to UTC before cutting the buckets, so a day starts at midnight where the reader is. 120 for UTC+2.

**Returns**

- `200` `WebhookStats`: The totals, the series and the breakdowns.

**Errors**

- The errors every operation can return: `400`, `401`, `403`, `404`, `422`, `500`, described in the [error catalog](https://openemail.uk/docs/api/errors).

Also available in: SDK [`webhooks.stats()`](https://openemail.uk/docs/sdk/reference/webhooks#stats); CLI [`openemail webhooks stats`](https://openemail.uk/docs/cli/reference/webhooks#webhooks-stats); MCP [`getWebhookStats`](https://openemail.uk/docs/mcp/tools/keys#getWebhookStats).

### Objects

#### `AuditActor`

`object`

Who made the change: a person, or a key acting over the API. Null when OpenEmail made it on its own, such as switching a webhook off after 100 failed events in a row, and when the person or key has since been deleted.

nullable

- `kind` (`string`, one of `"user"`, `"apiKey"`)
- `id` (`string`): The account id of the person, or the id of the key.
- `name` (`string`): The name of the person, or `API key <name>` for a key.
- `username` (`string`, nullable)
- `label` (`string`): What the app shows: `@username` for a person who has a username, otherwise `name`.

#### `WebhookActivityEntry`

`object`

- `object` (`string`, one of `"webhook_event"`)
- `id` (`string`)
- `endpointId` (`string`): The endpoint the change was made to. A removed endpoint keeps its history.
- `endpointLabel` (`string`): The host the endpoint posts to, or the host it posted to before it was removed.
- `type` (`string`, one of `"created"`, `"updated"`, `"enabled"`, `"disabled"`, `"auto_disabled"`, `"secret_rotated"`, `"tested"`, `"replayed"`, `"removed"`)
- `createdAt` (`string`, format `date-time`)
- `actor` (`AuditActor`)
- `detail` (`object`): What changed: the URL, and for an update the fields that moved, `previousUrl` when the URL did. A test or a replay also carries the status and response code it got.

#### `WebhookActivityList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`WebhookActivityEntry[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): An opaque cursor for the next page, or null on the last page. Pass it back unchanged.

#### `WebhookCatalogue`

`object`

- `object` (`string`, required, one of `"webhook_catalogue"`)
- `events` (`object[]`, required)
  - `id` (`string`, required, one of `"email.received"`, `"email.replied"`, `"email.sent"`, `"email.failed"`, `"email.cancelled"`, `"email.scheduled"`, `"email.queued"`, `"email.delivered"`, `"email.delivery_delayed"`, `"email.bounced"`, `"email.complained"`, `"email.suppressed"`, `"email.opened"`, `"email.clicked"`, `"email.downloaded"`, `"domain.verified"`, `"domain.sending_changed"`, `"domain.deleted"`, `"suppression.added"`, `"suppression.removed"`, `"file.uploaded"`, `"file.deleted"`, `"form.submitted"`, `"form.confirmed"`)
  - `label` (`string`, required): When the event fires, in a sentence.
- `maxEndpoints` (`integer`, required): How many endpoints this workspace may have, which its plan decides.
- `maxAddresses` (`integer`, required): How many addresses one allowlist may name, 50.
- `maxDomains` (`integer`, required): How many domains one allowlist may name, 25.

#### `WebhookDelivery`

`object`

- `object` (`string`, one of `"webhook_delivery"`)
- `id` (`string`): `whd_` followed by 24 hex characters.
- `endpointId` (`string`): The endpoint this attempt was sent to.
- `eventType` (`string`, one of `"email.received"`, `"email.replied"`, `"email.sent"`, `"email.failed"`, `"email.cancelled"`, `"email.scheduled"`, `"email.queued"`, `"email.delivered"`, `"email.delivery_delayed"`, `"email.bounced"`, `"email.complained"`, `"email.suppressed"`, `"email.opened"`, `"email.clicked"`, `"email.downloaded"`, `"domain.verified"`, `"domain.sending_changed"`, `"domain.deleted"`, `"suppression.added"`, `"suppression.removed"`, `"file.uploaded"`, `"file.deleted"`, `"form.submitted"`, `"form.confirmed"`)
- `eventId` (`string`, nullable): The payload `id`, the same on every retry and replay of one event.
- `status` (`string`, one of `"delivered"`, `"failed"`)
- `responseCode` (`integer`, nullable): Null when no response arrived at all, such as a timeout or a DNS failure.
- `durationMs` (`integer`, nullable)
- `attempt` (`integer`, nullable)
- `maxAttempts` (`integer`, nullable)
- `error` (`string`, nullable)
- `createdAt` (`string`, format `date-time`)
- `nextAttemptAt` (`string`, nullable, format `date-time`): When the automatic retry that follows this attempt is due. Null when none is waiting.

#### `WebhookDeliveryList`

`object`

- `object` (`string`, one of `"list"`)
- `data` (`WebhookDelivery[]`)
- `hasMore` (`boolean`): True when another page follows. Pass `nextCursor` back as `cursor` to read it.
- `nextCursor` (`string`, nullable): The id of the last delivery on this page, or null on the last page.

#### `WebhookStats`

`object`

- `object` (`string`, required, one of `"webhook_stats"`)
- `since` (`string`, required, format `date-time`)
- `until` (`string`, required, format `date-time`)
- `grain` (`string`, required, one of `"minute"`, `"hour"`, `"day"`)
- `endpointIds` (`string[]`, required): The endpoints asked for. Empty means every endpoint.
- `totals` (`object`, required)
  - `attempts` (`integer`)
  - `delivered` (`integer`)
  - `failed` (`integer`)
  - `medianDurationMs` (`number`, nullable): Null when nothing was sent in the window.
- `buckets` (`object[]`, required)
  - `bucket` (`string`)
  - `delivered` (`integer`)
  - `failed` (`integer`)
- `events` (`object[]`, required)
  - `id` (`string`)
  - `count` (`integer`)
- `codes` (`object[]`, required)
  - `id` (`string`): The HTTP status the receiver answered with.
  - `count` (`integer`)
