---
title: "Webhooks"
description: "The endpoints registered to receive events. Read only here."
canonical: "https://boltclaw.ai/docs/api/webhooks"
last-updated: "2026-11-05"
---

# Webhooks

The endpoints registered to receive events. Read only here.

These are the operations. **The event catalog, the payload shape, how to verify
a delivery and what happens when yours stops answering are on
[Webhooks](https://boltclaw.ai/docs/webhooks)** — this page is the endpoint reference for managing
endpoints, not the guide to receiving from them.

Reading needs `webhooks:read` and writing needs `webhooks:write`. Neither can
be held by an AI assistant's key: pointing a workspace's data at a new address
is a thing a person does, or a system a person deployed.

## Operations

### `GET /v1/webhooks/events`

**List webhook event types**

Scope `webhooks:read`.

Every event an endpoint can subscribe to, with the object each one is about. Read this
rather than hard-coding the list: an event added later appears here, and an endpoint that
subscribed to everything receives it without being changed.

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `WebhookEventTypeList` | The catalog, in the order the dashboard shows it. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `GET /v1/webhooks`

**List webhook endpoints**

Scope `webhooks:read`.

The endpoints registered for this workspace, to see where events go and why one did not
arrive.

Needs `webhooks:read`. A key issued with `inquiries:read` before this operation had a scope
of its own is still accepted here, and that acceptance will be withdrawn: issue new keys
with `webhooks:read`.

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `WebhookList` | The endpoints. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/webhooks`

**Register a webhook endpoint**

Scope `webhooks:write`. `Idempotency-Key` is **required**.

Register a URL and receive the signing secret, once.

The URL must be `https` and must resolve to a public host. Loopback, private ranges and
bare IP addresses are refused here and again at every delivery, because a hostname that is
public today can point somewhere else tomorrow.

`events` left out means every event, now and in future, which is what you want unless you
have a reason not to. An explicit list is validated against the catalog and an unknown name
is refused rather than dropped. `page_slugs` left out means every page in the workspace;
events that are not about a page are only ever sent to an endpoint that covers all of them.

`Idempotency-Key` is required. Minting a credential is the one operation where a retried
network timeout cannot be told apart from a request that never landed, and the only safe
move without a key would be to create a second endpoint. A replay answers the same body
with `secret: null`.

Five endpoints per workspace by default (`409`); your plan's number is
`webhook_endpoints` in `GET /usage`.

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `url` | `uri` | An `https` URL at a public host. Loopback, private ranges and bare IP addresses are refused. (**required**; min 12; max 2048) |
| `events` | `array \| null` | Which events this endpoint hears. Leave it out, or send null, for every event now and in future. An unknown name is refused rather than dropped. (<= 100 items) |
| `page_slugs` | `array \| null` | Which pages this endpoint hears about. Leave it out, or send null, for every page now and in future. Events that are not about a page only ever reach an endpoint that covers all of them. (<= 100 items) |
| `description` | `string \| null` | (max 100) |
| `test_events` | `boolean` | Also receive TEST events, each with `"test": true` in its body (default false). |

```json
{
  "url": "https://example.com"
}
```

**Answers**

| Status | Body |  |
|---|---|---|
| 201 | `WebhookCreated` | Created. `secret` is the signing secret, and this is the only response that carries it. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 413 | `ErrorEnvelope` | `PAYLOAD_TOO_LARGE`: the body is over 64 KB. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**Read one webhook endpoint**

Scope `webhooks:read`. Another workspace's answers `404`.

One endpoint's configuration and its delivery health. Never its signing secret.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `Webhook` | The endpoint. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**Change a webhook endpoint**

Scope `webhooks:write`. Another workspace's answers `404`.

Change the URL, the events, the pages, the description, or switch the endpoint on and off.
Send only what you want to change.

`enabled: false` is a pause: the endpoint and its signing secret survive, and events stop.
That is what you want while a receiver is being redeployed: deleting the endpoint would
destroy the secret every other environment is still using. `enabled: true` also clears the
failure count, or the next single failure would switch it off again however long it had
been working in between.

Events missed while an endpoint is off are not replayed.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Request body** (required)

| Field | Type |  |
|---|---|---|
| `url` | `uri` | (min 12; max 2048) |
| `events` | `array \| null` | (<= 100 items) |
| `page_slugs` | `array \| null` | (<= 100 items) |
| `description` | `string \| null` | (max 100) |
| `enabled` | `boolean` | False pauses the endpoint, keeping it and its signing secret. True resumes it and clears the failure count. |
| `test_events` | `boolean` | Also receive TEST events, each with `"test": true` in its body. |

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `Webhook` | The endpoint as it now is. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 413 | `ErrorEnvelope` | `PAYLOAD_TOO_LARGE`: the body is over 64 KB. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**Delete a webhook endpoint**

Scope `webhooks:write`. Another workspace's answers `404`.

Deletes the endpoint, its queued deliveries and its delivery log. The signing secret goes
with it and cannot be recovered.

To stop events without losing any of that, send `enabled: false` instead.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `WebhookDeleted` | Deleted. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**Rotate a signing secret**

Scope `webhooks:write`. `Idempotency-Key` is **required**. Another workspace's answers `404`.

Replace the signing secret and receive the new one, once. The endpoint keeps its id, its
URL and its subscriptions.

**There is no grace window, and that is deliberate.** Rotating an API key leaves the old
one working for a while because many systems hold it and we are the ones verifying it. A
signing secret is verified by one receiver you control, so a window in which two secrets
both validate is a window in which a leaked secret still validates, which is the only
reason anybody rotates one in a hurry. The old secret stops at once: paste the new one into
your receiver, and the deliveries in between fail their signature check and are retried.

`Idempotency-Key` is required, for the same reason it is on registering an endpoint. A
replay answers `secret: null`.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Request body** (optional)



**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `WebhookSecretRotated` | Rotated. `secret` is the new signing secret, and the only response carrying it. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 413 | `ErrorEnvelope` | `PAYLOAD_TOO_LARGE`: the body is over 64 KB. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**List delivery attempts**

Scope `webhooks:read`. Another workspace's answers `404`.

One row per attempt, newest first: the exact body that was signed, the status your endpoint
answered with, up to 512 bytes of that answer, how long it took, and why it failed when it
did.

`delivery_id` is stable across every attempt of one occurrence, and is the value the
`X-BoltClaw-Delivery` header carries. Key your idempotency off it.

Attempts are kept for 30 days. The reason for a failure is one of a fixed set of sentences,
never your own server's words.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |
| `limit` | query | `integer` | Page size, 1 to 100. Above 100 is clamped; anything else is the default. (default `50`; >= 1; <= 100) |
| `event` | query | `WebhookEvent` | Only attempts for this event. An unknown name is `400`. (one of `conversation.message.created`, `social.inbox.item.created`, `social.post.published`, `social.post.failed`, `lead.created`, `referral.partner.applied`, `referral.partner.approved`, `referral.conversion.created`, `referral.commission.created`, `referral.bonus.created`, `referral.referrer.created`, `referral.referrer.conversion`, `referral.referrer.reward_earned`, `referral.referrer.payout_details_changed`, `referral.commission.reversed`, `referral.payout.paid`, `referral.grant.ready`, `referral.grant.fulfilled`, `referral.grant.cancelled`, `referral.grant.reversed`, `referral.referee.invite_ready`, `referral.risk.alert`, `referral.payout.sent`, `referral.payout.failed`, `referral.payout.returned`, `referral.payout.requested`, `referral.tax_form.signed`, `referral.tax_form.expiring`, `referral.invoice.issued`, `referral.invoice.disputed`, `referral.broadcast.sent`, `referral.partner.imported_claimed`, `referral.contest.ended`, `referral.milestone.reached`, `referral.waitlist.entry_created`, `referral.waitlist.position_changed`, `webhook.test`) |

**Answers**

| Status | Body |  |
|---|---|---|
| 200 | `WebhookDeliveryList` | The attempts. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

### `POST /v1/webhooks/{id}/deliveries/{delivery_id}/redeliver`

**Send a delivery again**

Scope `webhooks:write`. Accepts `Idempotency-Key`. Another workspace's answers `404`.

Queue the same body again, signed afresh. The retry ladder starts over, and the body is the
one that was frozen when the event happened, so your receiver sees the event it already
knows how to deduplicate rather than a second, different fact.

Sixty redeliveries per workspace per hour (`429`). A paused endpoint is `409`.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |
| `delivery_id` | path | `uuid` | The `delivery_id` of an attempt. Anything that is not one of yours is `404`. (**required**) |

**Request body** (optional)



**Answers**

| Status | Body |  |
|---|---|---|
| 202 | `WebhookQueued` | Queued. The delivery has not happened yet. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 413 | `ErrorEnvelope` | `PAYLOAD_TOO_LARGE`: the body is over 64 KB. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

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

**Send a test event**

Scope `webhooks:write`. Accepts `Idempotency-Key`. Another workspace's answers `404`.

Queue a `webhook.test` delivery to this one endpoint, signed exactly as a real event is, so
you can prove your receiver verifies a signature before a real event arrives to tell you it
does not.

It is sent whatever the endpoint has subscribed to and whatever pages it covers: a test you
have to subscribe to first cannot tell you whether your subscription works. A paused
endpoint is `409`.

**Parameters**

| Name | In | Type |  |
|---|---|---|---|
| `id` | path | `uuid` | An id this API gave you. Anything that is not one of yours is `404`. (**required**) |

**Request body** (optional)



**Answers**

| Status | Body |  |
|---|---|---|
| 202 | `WebhookTestQueued` | Queued. The delivery has not happened yet. |
| 400 | `ErrorEnvelope` | `VALIDATION_ERROR`: a body that is not JSON, a field that breaks its rule (named in `fields`), a bad query value or cursor, or a malformed `Idempotency-Key`. |
| 401 | `ErrorEnvelope` | `UNAUTHORIZED`: no key, or one that is malformed, unknown or revoked. It never says which. |
| 403 | `ErrorEnvelope` | `INSUFFICIENT_SCOPE`: the key lacks this operation's scope. Or `ORGANISATION_SUSPENDED`: the key's organisation is suspended. Never about another workspace's object, which is 404. |
| 404 | `ErrorEnvelope` | `NOT_FOUND`: no such object, or it belongs to another workspace. The two are deliberately the same answer and never 403. If the id is right, check you are using the right key. |
| 409 | `ErrorEnvelope` | `CONFLICT`: the request clashes with the object's current state (the message says how). `IDEMPOTENCY_KEY_CONFLICT` / `IDEMPOTENCY_KEY_IN_FLIGHT`: the `Idempotency-Key` was used with another request, or its first request is still running. |
| 413 | `ErrorEnvelope` | `PAYLOAD_TOO_LARGE`: the body is over 64 KB. |
| 429 | `ErrorEnvelope` | `RATE_LIMITED`: too many requests. Wait for `Retry-After` seconds. |
| 500 | `ErrorEnvelope` | `INTERNAL_ERROR`: ours. Safe to retry; send the same `Idempotency-Key` on a POST. |

## Shapes

The objects these operations send and answer.

### EmptyBody

This operation takes no fields. Send `{}`, or no body at all.

`object`.

### Webhook

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `url` | `uri` | (**required**) |
| `events` | `array \| null` | Null for every event, now and in future. (**required**) |
| `description` | `string \| null` | (**required**) |
| `page_slugs` | `array \| null` | Null for every page, now and in future. (**required**) |
| `enabled` | `boolean` | False while the endpoint is paused. Events are not queued and are not replayed later. (**required**) |
| `disabled_at` | `date-time \| null` | (**required**) |
| `consecutive_failures` | `integer` | (**required**) |
| `last_success_at` | `date-time \| null` | (**required**) |
| `created_at` | `date-time` | (**required**) |
| `test_events` | `boolean` | Whether this endpoint also receives TEST events (referral test sales, test-mode and sandbox events, `test: true` conversions). A test delivery's body carries `"test": true`; a live one never does. |

### WebhookCreate

| Field | Type |  |
|---|---|---|
| `url` | `uri` | An `https` URL at a public host. Loopback, private ranges and bare IP addresses are refused. (**required**; min 12; max 2048) |
| `events` | `array \| null` | Which events this endpoint hears. Leave it out, or send null, for every event now and in future. An unknown name is refused rather than dropped. (<= 100 items) |
| `page_slugs` | `array \| null` | Which pages this endpoint hears about. Leave it out, or send null, for every page now and in future. Events that are not about a page only ever reach an endpoint that covers all of them. (<= 100 items) |
| `description` | `string \| null` | (max 100) |
| `test_events` | `boolean` | Also receive TEST events, each with `"test": true` in its body (default false). |

### WebhookCreated

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `url` | `uri` | (**required**) |
| `events` | `array \| null` | Null for every event, now and in future. (**required**) |
| `description` | `string \| null` | (**required**) |
| `page_slugs` | `array \| null` | Null for every page, now and in future. (**required**) |
| `enabled` | `boolean` | False while the endpoint is paused. Events are not queued and are not replayed later. (**required**) |
| `disabled_at` | `date-time \| null` | (**required**) |
| `consecutive_failures` | `integer` | (**required**) |
| `last_success_at` | `date-time \| null` | (**required**) |
| `created_at` | `date-time` | (**required**) |
| `test_events` | `boolean` | Whether this endpoint also receives TEST events (referral test sales, test-mode and sandbox events, `test: true` conversions). A test delivery's body carries `"test": true`; a live one never does. |
| `secret` | `string \| null` | The signing secret, shown once. `null` on an idempotent replay: the endpoint exists, and the value is not stored anywhere it could be read again. Rotate it if you did not record it. (**required**) |
| `warning` | `string` | (**required**) |

### WebhookDeleted

| Field | Type |  |
|---|---|---|
| `deleted` | `boolean` | (**required**; one of `true`) |
| `id` | `uuid` | (**required**) |

### WebhookDelivery

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `delivery_id` | `uuid` | Stable across every attempt of one occurrence, and the value the `X-BoltClaw-Delivery` header carries. Key your idempotency off it. (**required**) |
| `event` | `WebhookEvent` | An event an endpoint can subscribe to. `GET /webhooks/events` returns the live list with a description of each; read that rather than hard-coding this enum, because an event added later appears there first. (**required**; one of `conversation.message.created`, `social.inbox.item.created`, `social.post.published`, `social.post.failed`, `lead.created`, `referral.partner.applied`, `referral.partner.approved`, `referral.conversion.created`, `referral.commission.created`, `referral.bonus.created`, `referral.referrer.created`, `referral.referrer.conversion`, `referral.referrer.reward_earned`, `referral.referrer.payout_details_changed`, `referral.commission.reversed`, `referral.payout.paid`, `referral.grant.ready`, `referral.grant.fulfilled`, `referral.grant.cancelled`, `referral.grant.reversed`, `referral.referee.invite_ready`, `referral.risk.alert`, `referral.payout.sent`, `referral.payout.failed`, `referral.payout.returned`, `referral.payout.requested`, `referral.tax_form.signed`, `referral.tax_form.expiring`, `referral.invoice.issued`, `referral.invoice.disputed`, `referral.broadcast.sent`, `referral.partner.imported_claimed`, `referral.contest.ended`, `referral.milestone.reached`, `referral.waitlist.entry_created`, `referral.waitlist.position_changed`, `webhook.test`) |
| `attempt` | `integer` | (**required**; >= 1) |
| `payload` | `object` | The exact body that was signed and sent. Identifiers only, by design. (**required**) |
| `status` | `string` | (**required**; one of `delivered`, `failed`) |
| `response_status` | `integer \| null` | (**required**) |
| `response_excerpt` | `string \| null` | At most 512 bytes of what your endpoint answered. Null when there was nothing to keep, or when the address finally reached was not one we would deliver to. (**required**) |
| `error` | `string \| null` | Why it failed, as one of a fixed set of sentences. Null when it succeeded. Never your own server's words. (**required**) |
| `duration_ms` | `integer \| null` | (**required**) |
| `created_at` | `date-time` | (**required**) |

### WebhookDeliveryList

| Field | Type |  |
|---|---|---|
| `data` | `WebhookDelivery[]` | (**required**) |

### WebhookEvent

An event an endpoint can subscribe to. `GET /webhooks/events` returns the live list with a
description of each; read that rather than hard-coding this enum, because an event added
later appears there first.

One of: `conversation.message.created`, `social.inbox.item.created`, `social.post.published`, `social.post.failed`, `lead.created`, `referral.partner.applied`, `referral.partner.approved`, `referral.conversion.created`, `referral.commission.created`, `referral.bonus.created`, `referral.referrer.created`, `referral.referrer.conversion`, `referral.referrer.reward_earned`, `referral.referrer.payout_details_changed`, `referral.commission.reversed`, `referral.payout.paid`, `referral.grant.ready`, `referral.grant.fulfilled`, `referral.grant.cancelled`, `referral.grant.reversed`, `referral.referee.invite_ready`, `referral.risk.alert`, `referral.payout.sent`, `referral.payout.failed`, `referral.payout.returned`, `referral.payout.requested`, `referral.tax_form.signed`, `referral.tax_form.expiring`, `referral.invoice.issued`, `referral.invoice.disputed`, `referral.broadcast.sent`, `referral.partner.imported_claimed`, `referral.contest.ended`, `referral.milestone.reached`, `referral.waitlist.entry_created`, `referral.waitlist.position_changed`, `webhook.test`.

### WebhookEventType

| Field | Type |  |
|---|---|---|
| `event` | `WebhookEvent` | An event an endpoint can subscribe to. `GET /webhooks/events` returns the live list with a description of each; read that rather than hard-coding this enum, because an event added later appears there first. (**required**; one of `conversation.message.created`, `social.inbox.item.created`, `social.post.published`, `social.post.failed`, `lead.created`, `referral.partner.applied`, `referral.partner.approved`, `referral.conversion.created`, `referral.commission.created`, `referral.bonus.created`, `referral.referrer.created`, `referral.referrer.conversion`, `referral.referrer.reward_earned`, `referral.referrer.payout_details_changed`, `referral.commission.reversed`, `referral.payout.paid`, `referral.grant.ready`, `referral.grant.fulfilled`, `referral.grant.cancelled`, `referral.grant.reversed`, `referral.referee.invite_ready`, `referral.risk.alert`, `referral.payout.sent`, `referral.payout.failed`, `referral.payout.returned`, `referral.payout.requested`, `referral.tax_form.signed`, `referral.tax_form.expiring`, `referral.invoice.issued`, `referral.invoice.disputed`, `referral.broadcast.sent`, `referral.partner.imported_claimed`, `referral.contest.ended`, `referral.milestone.reached`, `referral.waitlist.entry_created`, `referral.waitlist.position_changed`, `webhook.test`) |
| `resource` | `string` | What the event is about, and what you fetch to get the content. (**required**) |
| `label` | `string` | (**required**) |
| `description` | `string` | (**required**) |

### WebhookEventTypeList

| Field | Type |  |
|---|---|---|
| `data` | `WebhookEventType[]` | (**required**) |

### WebhookList

| Field | Type |  |
|---|---|---|
| `data` | `Webhook[]` | (**required**) |

### WebhookQueued

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `status` | `string` | (**required**; one of `pending`) |
| `delivery_id` | `uuid` | The new occurrence id. The body is the one that was frozen when the event happened. (**required**) |

### WebhookSecretRotated

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `secret` | `string \| null` | The new signing secret, shown once. `null` on an idempotent replay. (**required**) |
| `warning` | `string` | (**required**) |

### WebhookTestQueued

| Field | Type |  |
|---|---|---|
| `id` | `uuid` | (**required**) |
| `event` | `string` | (**required**; one of `webhook.test`) |
| `status` | `string` | (**required**; one of `pending`) |

### WebhookUpdate

Send only the fields you want to change. An empty object is `400`.

| Field | Type |  |
|---|---|---|
| `url` | `uri` | (min 12; max 2048) |
| `events` | `array \| null` | (<= 100 items) |
| `page_slugs` | `array \| null` | (<= 100 items) |
| `description` | `string \| null` | (max 100) |
| `enabled` | `boolean` | False pauses the endpoint, keeping it and its signing secret. True resumes it and clears the failure count. |
| `test_events` | `boolean` | Also receive TEST events, each with `"test": true` in its body. |

<!-- https://boltclaw.ai/docs/api/webhooks as markdown. Generated from openapi.yaml; API version 1.22.0. -->
<!-- Every /docs page answers as markdown: add .md to its path, or send Accept: text/markdown. -->
