# Webhooks

> Every field of /v1/webhooks: register, list, read, update, delete, rotate the secret, send a test event and read the delivery history.

<!-- Generated by apps/docs/scripts/gen-reference.mjs from apps/docs/public/openapi/v1.json. Do not edit: change the API's schemas (then `pnpm --filter @seasalt/api openapi:emit`) or the notes in apps/docs/src/reference-notes/, then run `pnpm --filter @seasalt/docs gen:reference`. -->

Webhook endpoints: where events are delivered and how they are signed. How-to: [Receive webhooks](/platform-api/events/).

Generated from [`/openapi/v1.json`](/openapi/v1.json); interactive version: [API reference](/openapi/v1/).

## Endpoints

| method | path | what it does | success |
|---|---|---|---|
| [`GET`](#get-v1webhooks) | `/v1/webhooks` | List webhook endpoints | `200` |
| [`POST`](#post-v1webhooks) | `/v1/webhooks` | Register a webhook endpoint | `201` |
| [`GET`](#get-v1webhooksid) | `/v1/webhooks/{id}` | Retrieve a webhook endpoint | `200` |
| [`PATCH`](#patch-v1webhooksid) | `/v1/webhooks/{id}` | Update a webhook endpoint | `200` |
| [`DELETE`](#delete-v1webhooksid) | `/v1/webhooks/{id}` | Delete a webhook endpoint | `204` |
| [`POST`](#post-v1webhooksidrotate-secret) | `/v1/webhooks/{id}/rotate-secret` | Change a webhook endpoint's signing secret | `200` |
| [`POST`](#post-v1webhooksidtest) | `/v1/webhooks/{id}/test` | Send a test event | `200` |
| [`GET`](#get-v1webhooksiddeliveries) | `/v1/webhooks/{id}/deliveries` | List an endpoint's deliveries | `200` |
| [`GET`](#get-v1webhooksiddeliveriesdelivery_id) | `/v1/webhooks/{id}/deliveries/{delivery_id}` | Retrieve a delivery | `200` |

## `GET /v1/webhooks`

List webhook endpoints.

List the workspace's webhook endpoints, newest first, in pages (`has_more`, `next_cursor`; pass it back as `after`). `secret` is never included. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `limit` | query | integer<br>1–200 | Max endpoints to return (1–200, default 100). Out of range → 400. |
| `after` | query | string | The previous page's `next_cursor`. |

</div>

**Answers** `200` with [WebhookEndpointList](#webhookendpointlist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

## `POST /v1/webhooks`

Register a webhook endpoint.

Subscribe a public https URL of at most 2,048 characters to a set of workspace event types. The response includes a one-time `secret` used to verify delivery signatures (Standard Webhooks / svix scheme) — it is never returned again. Deliveries are signed. A delivery is attempted at once; one not answered 2xx within 10 s is retried on the Standard Webhooks schedule (after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h and 10 h: 8 attempts over about 27.6 h), then given up; an endpoint with 20 given-up deliveries in a row is disabled (re-enable with `PATCH { enabled: true }`). Webhooks can arrive out of order: order by the envelope `seq`, dedupe by `id`. A new endpoint receives v2 payloads (the `WorkspaceEventV2` schemas) unless `api_version` is `2026-10-01`. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Request body:** [CreateWebhookRequest](#createwebhookrequest).

**Answers** `201` with [WebhookEndpoint](#webhookendpoint). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Retrieve a webhook endpoint.

Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |

</div>

**Answers** `200` with [WebhookEndpointRead](#webhookendpointread). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Update a webhook endpoint.

Change the `url`, `event_types`, `description` or `api_version`, or toggle `enabled`. Re-enabling clears the failure state. A new `api_version` applies to deliveries not yet attempted. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |

</div>

**Request body:** [UpdateWebhookRequest](#updatewebhookrequest).

**Answers** `200` with [WebhookEndpointRead](#webhookendpointread). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Delete a webhook endpoint.

Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |

</div>

**Answers** `204` with no body. Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Change a webhook endpoint's signing secret.

Replaces the signing secret and returns the new one once. The old secret stops working at once: every later delivery attempt, retries included, is signed with the new one, so a receiver that still checks the old secret rejects them (and they count as failed deliveries) until it is updated. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |

</div>

**Answers** `200` with [WebhookEndpoint](#webhookendpoint). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Send a test event.

POSTs one sample `webhook.test` event to the endpoint, synchronously, signed with its current secret like a real delivery (10 s timeout, no redirects), and returns what the receiver answered. Works on a disabled endpoint too. Nothing is stored: no event in `/v1/events`, no delivery, no change to the failure count. At most 10 tests per workspace per minute (`429 webhook_test_rate_limited`). Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |

</div>

**Answers** `200` with [WebhookTestResult](#webhooktestresult). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

List an endpoint's deliveries.

The endpoint's deliveries of the last 7 days, newest first, in pages (`has_more`, `next_cursor`; pass it back as `after`, with the same `status`). Each one says whether it succeeded, is retrying or was given up, how often it was tried, and what the receiver answered (`response_status`, `error`). The reply body and the request body are on `GET /v1/webhooks/{id}/deliveries/{delivery_id}`. Nothing is re-sent: replay an event from `GET /v1/events?after=<seq - 1>` (with `&api_version=v2` for the v2 body). Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |
| `limit` | query | integer<br>1–100 | Max deliveries to return (1–100, default 50). Out of range → 400. |
| `after` | query | string | The previous page's `next_cursor`. A cursor of another endpoint or another `status` → 400 `invalid_cursor`. |
| `status` | query | string<br>`all`, `pending`, `succeeded`, `failed`, `exhausted`, `problems`; default `all` | Only deliveries in this state. `problems` is `failed` and `exhausted` together, without the deliveries dropped because the endpoint was off or the event expired (`error.code` `endpoint_disabled` / `retention`). Anything else → 400. |

</div>

**Answers** `200` with [WebhookDeliveryList](#webhookdeliverylist). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

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

Retrieve a delivery.

One delivery, with the exact body that was (or will be) POSTed (`payload`), the headers of the latest try without the signature, and the first 1,000 characters of the receiver's reply on a failed try (`response_excerpt`). An endpoint of another workspace, a delivery of another endpoint, and a missing one are the same `404 not_found`. Owners and admins only: a key whose owner has the Agent role in the workspace gets `403 insufficient_role`.

**Parameters**

<div class="ref-table">

| name | in | type | description |
|---|---|---|---|
| `id` | path | string, required | The endpoint short id (`whe-…`) or UUID. |
| `delivery_id` | path | string (uuid), required | The delivery's `id`. |

</div>

**Answers** `200` with [WebhookDeliveryDetail](#webhookdeliverydetail). Errors use the [error envelope](/platform-api/reference/#v1error); common codes: [webhooks](/platform-api/errors/#webhooks), [every request](/platform-api/errors/#every-request).

## Objects

### WebhookEndpoint

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | Short id, e.g. "whe-0000000001". |
| `object` | string, required<br>always `webhook_endpoint` |   |
| `url` | string (uri), required |   |
| `event_types` | array of string, required | Subscribed types, or `["*"]` for all. |
| `enabled` | boolean, required |   |
| `api_version` | string, required<br>`2026-10-01`, `v2` | The payload version this endpoint receives: `v2` (`WorkspaceEventV2`, the default for a new endpoint) or `2026-10-01` (`WorkspaceEvent`, the legacy format every endpoint that existed before release v1.84.0.0 keeps). A change applies to deliveries not yet attempted; a delivery in retry keeps the version it was first sent in. |
| `created_at` | string (date-time), required | When the endpoint was created (ISO 8601). |
| `updated_at` | string (date-time), required | When the endpoint was last changed (ISO 8601). |
| `description` | string or null |   |
| `secret` | string | Signing secret ("whsec_…"). Returned ONLY by the create response and by `POST /v1/webhooks/{id}/rotate-secret` — never on reads. |

</div>

Example: see [Receive webhooks](/platform-api/events/).

### WebhookEndpointRead

A webhook endpoint as list, read and update answer it: never with its `secret` (only create and `rotate-secret` return that).

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string, required | Short id, e.g. "whe-0000000001". |
| `object` | string, required<br>always `webhook_endpoint` |   |
| `url` | string (uri), required |   |
| `event_types` | array of string, required | Subscribed types, or `["*"]` for all. |
| `enabled` | boolean, required |   |
| `api_version` | string, required<br>`2026-10-01`, `v2` | The payload version this endpoint receives: `v2` (`WorkspaceEventV2`, the default for a new endpoint) or `2026-10-01` (`WorkspaceEvent`, the legacy format every endpoint that existed before release v1.84.0.0 keeps). A change applies to deliveries not yet attempted; a delivery in retry keeps the version it was first sent in. |
| `created_at` | string (date-time), required | When the endpoint was created (ISO 8601). |
| `updated_at` | string (date-time), required | When the endpoint was last changed (ISO 8601). |
| `description` | string or null |   |

</div>

Example: see [Receive webhooks](/platform-api/events/).

### WebhookEndpointList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [WebhookEndpointRead](#webhookendpointread), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; null on the last. |

</div>

Example: see [Receive webhooks](/platform-api/events/).

### CreateWebhookRequest

<div class="ref-table">

| field | type | description |
|---|---|---|
| `url` | string (uri), required | Public https endpoint. Rejected (400) if it resolves to a private/loopback/link-local/metadata address. |
| `event_types` | array of string<br>each `conversation.created`, `turn.completed`, `confirmation.requested`, `confirmation.resolved`, `spend.recorded`, `run.completed`, `message.received`, `message.sent`, `message.status_updated`, `campaign.started`, `campaign.paused`, `campaign.resumed`, `campaign.completed`, `campaign.canceled`, `campaign.waiting`, `campaign.window_opened`, `meta_response.completed`, `meta_chat.updated`, `call.ringing`, `call.answered`, `call.completed`, `call.failed`, `*` | Optional; defaults to `["*"]` (all events). |
| `description` | string<br>≤ 500 chars |   |
| `api_version` | string<br>`2026-10-01`, `v2`; default `v2` | The payload version the endpoint receives: `v2` (default) or `2026-10-01` (legacy). See /platform-api/events/#payload-versions. |

</div>

```json
{
  "url": "https://example.com/file",
  "event_types": [
    "conversation.created"
  ],
  "api_version": "v2"
}
```

### UpdateWebhookRequest

<div class="ref-table">

| field | type | description |
|---|---|---|
| `url` | string (uri) |   |
| `event_types` | array of string<br>each `conversation.created`, `turn.completed`, `confirmation.requested`, `confirmation.resolved`, `spend.recorded`, `run.completed`, `message.received`, `message.sent`, `message.status_updated`, `campaign.started`, `campaign.paused`, `campaign.resumed`, `campaign.completed`, `campaign.canceled`, `campaign.waiting`, `campaign.window_opened`, `meta_response.completed`, `meta_chat.updated`, `call.ringing`, `call.answered`, `call.completed`, `call.failed`, `*` |   |
| `enabled` | boolean |   |
| `description` | string<br>≤ 500 chars |   |
| `api_version` | string<br>`2026-10-01`, `v2` | Switch the payload version: `v2` or `2026-10-01`. Left out, the endpoint keeps the one it has. See /platform-api/events/#payload-versions. |

</div>

```json
{
  "event_types": [
    "conversation.created"
  ],
  "api_version": "v2"
}
```

### WebhookTestResult

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `webhook_test` |   |
| `ok` | boolean, required | The receiver answered 2xx. |
| `status` | integer or null, required | The HTTP status, or null with no answer. |
| `status_text` | string or null, required | The status phrase, e.g. "OK". |
| `duration_ms` | integer, required |   |
| `error` | string or null, required<br>`timeout`, `connect`, `redirect` | Why no answer came: `timeout` (none within 10 s), `connect`, or `redirect` (redirects are not followed). Null when the receiver answered. |
| `response_excerpt` | string or null, required | The first 1,000 characters of the reply body, when there was one. |

</div>

```json
{
  "object": "webhook_test",
  "ok": false,
  "status": null,
  "status_text": null,
  "duration_ms": 1,
  "error": "timeout",
  "response_excerpt": null
}
```

### WebhookDelivery

One event's delivery to one endpoint. Deliveries are kept for 7 days. The list leaves out the reply and the body: read one delivery for those.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string (uuid), required | The delivery's id. |
| `object` | string, required<br>always `webhook_delivery` |   |
| `endpoint_id` | string, required | The endpoint's short id, e.g. "whe-0000000001". |
| `event_id` | string, required | The event's `id` (`evt_…`), also sent as the `webhook-id` header. |
| `event_type` | string or null, required | The event's type; null once the event left the 7-day log. |
| `seq` | integer, required | The event's position in the workspace's log. Replay from this event with `GET /v1/events?after=<seq - 1>`. |
| `status` | string, required<br>`pending`, `succeeded`, `failed`, `exhausted` | `pending` (not tried yet), `succeeded` (answered 2xx), `failed` (will be retried at `next_attempt_at`), `exhausted` (given up, or dropped: see `error.code`). |
| `attempts` | integer, required | Tries made so far (at most 8). |
| `next_attempt_at` | string (date-time) or null, required | When the next retry is due; set only when `status` is `failed`. |
| `response_status` | integer or null, required | The HTTP status of the latest try, or null when there was no answer. |
| `error` | object or null, required | Why the latest try failed, or why the delivery was dropped; null when it did not. |
| `error.code` | string, required<br>`endpoint_disabled`, `retention`, `timeout`, `redirect`, `connect`, `http`, `render` | `http` (answered, not 2xx), `timeout` (no answer within 10 s), `connect` (no connection), `redirect` (redirects are not followed), `render` (the body could not be built; nothing was sent), `endpoint_disabled` (dropped: the endpoint was off), `retention` (dropped: the event expired before it could be delivered). |
| `error.message` | string, required | The stored text, e.g. "HTTP 502". |
| `api_version` | string, required<br>`2026-10-01`, `v2` | The payload version this delivery was sent in (a retry resends it), or, not tried yet, will be sent in. |
| `created_at` | string (date-time), required |   |
| `updated_at` | string (date-time), required |   |
| `last_attempt_at` | string (date-time) or null, required | When the latest try was signed (its `webhook-timestamp`); null before the first. |

</div>

Example: see [Receive webhooks](/platform-api/events/).

### WebhookDeliveryDetail

One delivery, with the body sent and the receiver's reply.

<div class="ref-table">

| field | type | description |
|---|---|---|
| `id` | string (uuid), required | The delivery's id. |
| `object` | string, required<br>always `webhook_delivery` |   |
| `endpoint_id` | string, required | The endpoint's short id, e.g. "whe-0000000001". |
| `event_id` | string, required | The event's `id` (`evt_…`), also sent as the `webhook-id` header. |
| `event_type` | string or null, required | The event's type; null once the event left the 7-day log. |
| `seq` | integer, required | The event's position in the workspace's log. Replay from this event with `GET /v1/events?after=<seq - 1>`. |
| `status` | string, required<br>`pending`, `succeeded`, `failed`, `exhausted` | `pending` (not tried yet), `succeeded` (answered 2xx), `failed` (will be retried at `next_attempt_at`), `exhausted` (given up, or dropped: see `error.code`). |
| `attempts` | integer, required | Tries made so far (at most 8). |
| `next_attempt_at` | string (date-time) or null, required | When the next retry is due; set only when `status` is `failed`. |
| `response_status` | integer or null, required | The HTTP status of the latest try, or null when there was no answer. |
| `error` | object or null, required | Why the latest try failed, or why the delivery was dropped; null when it did not. |
| `error.code` | string, required<br>`endpoint_disabled`, `retention`, `timeout`, `redirect`, `connect`, `http`, `render` | `http` (answered, not 2xx), `timeout` (no answer within 10 s), `connect` (no connection), `redirect` (redirects are not followed), `render` (the body could not be built; nothing was sent), `endpoint_disabled` (dropped: the endpoint was off), `retention` (dropped: the event expired before it could be delivered). |
| `error.message` | string, required | The stored text, e.g. "HTTP 502". |
| `api_version` | string, required<br>`2026-10-01`, `v2` | The payload version this delivery was sent in (a retry resends it), or, not tried yet, will be sent in. |
| `created_at` | string (date-time), required |   |
| `updated_at` | string (date-time), required |   |
| `last_attempt_at` | string (date-time) or null, required | When the latest try was signed (its `webhook-timestamp`); null before the first. |
| `response_excerpt` | string or null, required | The first 1,000 characters of the receiver's reply on a failed try. |
| `payload` | map of string to any or null, required | The exact JSON body that was (or will be) POSTed, in `api_version`'s format. Null once the event left the 7-day log, or when the body could not be built. |
| `headers` | object, required | The headers of the latest try. `webhook-signature` is never returned. |
| `headers.webhook-id` | string, required |   |
| `headers.webhook-timestamp` | string or null, required | Unix seconds, as Standard Webhooks defines it; null before the first try. |

</div>

Example: see [Receive webhooks](/platform-api/events/).

### WebhookDeliveryList

<div class="ref-table">

| field | type | description |
|---|---|---|
| `object` | string, required<br>always `list` |   |
| `data` | array of [WebhookDelivery](#webhookdelivery), required |   |
| `has_more` | boolean, required |   |
| `next_cursor` | string or null, required | Pass as `after` for the next page; null on the last. |

</div>

Example: see [Receive webhooks](/platform-api/events/).