---
title: Webhook endpoints
url: https://base_url.placeholder/docs/api/webhooks
group: Surfaces
---

# Webhook endpoints

Register URLs to receive job events instead of polling, and read back what was delivered. At most 10 endpoints per account. Every delivery is signed with the endpoint's secret and retried until your receiver answers `2xx` or 24 hours have passed; the events, their bodies and how to verify a signature are on /docs/webhooks.

8 endpoints under `/api/v1/webhook-endpoints`. What every call shares is on [the REST overview](https://base_url.placeholder/docs/api#conventions), and what they are for — with the calls for every SDK, the CLI and MCP — on [Webhooks](https://base_url.placeholder/docs/webhooks). `*` marks a required field.

- GET /api/v1/webhook-endpoints — List webhook endpoints
- POST /api/v1/webhook-endpoints — Register a webhook endpoint
- GET /api/v1/webhook-endpoints/{id} — Get one webhook endpoint
- PUT /api/v1/webhook-endpoints/{id} — Update a webhook endpoint
- DELETE /api/v1/webhook-endpoints/{id} — Delete a webhook endpoint
- GET /api/v1/webhook-endpoints/{id}/deliveries — Recent deliveries to an endpoint
- POST /api/v1/webhook-endpoints/{id}/rotate-secret — Rotate the signing secret
- POST /api/v1/webhook-endpoints/{id}/test — Send a test delivery

## List webhook endpoints

GET /api/v1/webhook-endpoints

Every endpoint registered on this account, newest first. There are at most 10, so the list is not paged. The signing secret is masked (`secretHint`); it is returned in full only by create and rotate-secret. An endpoint the service switched off after repeated failures reads `enabled: false` with `disabledAt` and `disabledReason`.

### Returns `200` — `data` is `WebhookEndpoint[]`

## Register a webhook endpoint

POST /api/v1/webhook-endpoints

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Registers an `https://` URL to receive job events, and answers `201` with the endpoint.

**The response is the only place the signing secret is ever readable** (`secret`). Store it; a lost secret is rotated, not recovered. Every delivery carries `ImageStep-Signature: t=<unix seconds>,v1=<hex>`, where `v1` is `HMAC-SHA256("<t>.<raw body>", secret)`: compute it over the raw body, compare in constant time, and reject a `t` outside your tolerance window.

Omit `events`, or send `[]`, to receive every job-level event; per-item events are opt-in.

The URL must be on port 443 — the only port deliveries can reach — and resolve to a public address. The address is checked now and again before every delivery attempt, against what the host resolves to then, and redirects are not followed.

### Request body — `EndpointRequest`

### Returns `201` — `data` is `WebhookEndpoint`

Registered and enabled. The one answer that carries `secret` in the clear.

### Errors

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `url` — missing<br>- `invalid_param` on `url` — too long, not `https://`, a port other than 443, no host, or the host does not resolve to a public address (loopback, private, link-local and the other non-public ranges are refused)<br>- `invalid_param` on `events` — a type that is not a subscribable event; `details.received` is the one refused<br>- `invalid_param` on `description` — too long |
| 422 | `resource_limit_exceeded` — the account already has 10 endpoints; delete one first |

## Get one webhook endpoint

GET /api/v1/webhook-endpoints/{id}

One endpoint, its secret masked. `enabled`, `disabledAt`, `disabledReason` and `consecutiveFailures` say whether events are reaching it; the deliveries themselves are `GET /api/v1/webhook-endpoints/{id}/deliveries`.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Returns `200` — `data` is `WebhookEndpoint`

The endpoint

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Update a webhook endpoint

PUT /api/v1/webhook-endpoints/{id}

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Every field is optional; only what you send changes. A new `url` is checked as on create.

- `events: []` goes back to the default subscription, every job-level event.
- `enabled: false` pauses the endpoint: nothing new is queued for it, and events already queued are closed as `FAILED` (`endpoint disabled`) rather than held.
- `enabled: true` turns it back on and also clears an automatic disable — `disabledAt`, `disabledReason` and `consecutiveFailures`. `POST /{id}/test` is the cheap way to check the receiver first.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Request body — `EndpointRequest`

### Returns `200` — `data` is `WebhookEndpoint`

The endpoint as it now is

### Errors

| Status | Code and when |
| --- | --- |
| 400 | - `invalid_param` on `url` — too long, not `https://`, a port other than 443, no host, or the host does not resolve to a public address (loopback, private, link-local and the other non-public ranges are refused)<br>- `invalid_param` on `events` — a type that is not a subscribable event; `details.received` is the one refused<br>- `invalid_param` on `description` — too long |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Delete a webhook endpoint

DELETE /api/v1/webhook-endpoints/{id}

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Deletes the endpoint and answers `204` with no body. Nothing more is delivered to it: events still queued are closed as `FAILED` (`endpoint no longer exists`), and its delivery history is no longer readable. To stop deliveries and keep the endpoint, `PUT` it with `enabled: false`.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Returns `204` — no body

Deleted

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Recent deliveries to an endpoint

GET /api/v1/webhook-endpoints/{id}/deliveries

[Paged](https://base_url.placeholder/docs/errors#pagination) — `page` and `perPage`, or `cursor`, in; `meta` out

Newest first, with the attempt count, the HTTP status your receiver answered and the next scheduled retry. This is where to look when an event did not arrive. A row does not carry the event body — that is the document your receiver was sent, and `eventId` is what joins the two. Test deliveries are listed too. Records are kept 14 days.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Returns `200` — `data` is `WebhookDeliverySummary[]`

One page of deliveries

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Rotate the signing secret

POST /api/v1/webhook-endpoints/{id}/rotate-secret

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Replaces the signing secret and returns the new one in the clear, once (`secret`). The old secret stops verifying immediately: every attempt from now on — retries of events queued before the rotation included — is signed with the new one. A delivery your receiver refuses in the switch-over is retried on the normal schedule, so a receiver that answers non-`2xx` to a bad signature and picks up the new secret within minutes loses nothing.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Returns `200` — `data` is `WebhookEndpoint`

The endpoint, with the new `secret` in the clear

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Send a test delivery

POST /api/v1/webhook-endpoints/{id}/test

Accepts an [`Idempotency-Key`](https://base_url.placeholder/docs/errors#idempotency)

Posts a synthetic `webhook.test` event to the endpoint while you wait, signed like any delivery, and answers with the delivery record: `status` is `DELIVERED` when your receiver answered `2xx`, otherwise `FAILED` with `responseStatus` and `error`.

- One attempt: a failed test is not retried, and does not count towards the automatic disable. A `2xx` resets the endpoint's run of failed events without re-enabling it.
- It works on a disabled endpoint — the way to check a receiver before turning it back on.
- The attempt waits up to 5 s to connect and 10 s for the answer, so this call can take that long.

USE THIS WHEN: verifying a new receiver, or checking signature verification. DO NOT USE WHEN: replaying a real event — this one carries no job data.

### Parameters

| Name | In | Type | Description |
| --- | --- | --- | --- |
| id* | path | string | The endpoint's id (`whe_…`), as create and the listing return it |

### Returns `200` — `data` is `WebhookDeliverySummary`

The attempt was made — read `status`: an unreachable receiver is a `FAILED` delivery, not an error

### Errors

| Status | Code and when |
| --- | --- |
| 404 | `not_found` — no webhook endpoint with this id on your account |

## Objects

Each described once; a linked type is another object on this page.

### EndpointRequest

Webhook endpoint create/update payload

| Field | Type | Description |
| --- | --- | --- |
| description | string | Free-text label shown in listings<br>e.g. "Production n8n" |
| enabled | boolean | Update only — ignored on create, where an endpoint starts enabled. `false` pauses deliveries; `true` resumes them and clears an automatic disable. |
| events | string[] | Event types to receive. Omit it, or send `[]`, for every job-level event (`job.completed`, `job.failed`); the per-item events (`job.item.completed`, `job.item.failed`) are opt-in, because a 500-item job would otherwise be 500 deliveries nobody asked for. Any other value is `400 invalid_param`.<br>e.g. ["job.completed","job.failed"] |
| url | string | Where to POST events: an `https://` URL on port 443 whose host resolves to a public address. Required on create; on update, send it only to change it.<br>e.g. "https://example.com/hooks/imagestep" |

### WebhookDeliverySummary

One webhook delivery attempt: did it arrive, after how many tries, and what did the receiver say

| Field | Type | Description |
| --- | --- | --- |
| attempts | integer (int32) | How many HTTP attempts have been made |
| createdAt | string (date-time) | When the event was queued; retries stop 24 hours after it |
| deliveredAt | string (date-time) | When a 2xx came back |
| error | string | Why the last attempt failed, when it did: `HTTP 503`, a connection error, `target refused: …` when the host resolved to a non-public address (no request was sent), or `endpoint disabled` |
| eventId | string | The event this delivered; the same id is in the body's `id` and on every retry |
| eventType | string | Event type, e.g. job.completed |
| id | string | The delivery's id, `whd_…` |
| nextAttemptAt | string (date-time) | When the next retry is due, while status is PENDING |
| responseStatus | integer (int32) | The last attempt's HTTP status, or null when the request never got one (DNS, timeout, TLS) |
| status | string | PENDING while retries remain, DELIVERED once a 2xx came back, FAILED when it gave up — 24 hours after the event was queued, after a test's single attempt, or when the endpoint was disabled or deleted with the event still queued |

### WebhookEndpoint

A URL this account receives job events at. The signing secret is masked on every read but the create and rotate-secret answers.

| Field | Type | Description |
| --- | --- | --- |
| consecutiveFailures | integer (int32) | Events in a row whose delivery ran out of retries since the last accepted one. At 5 the endpoint is disabled; any `2xx` resets it to 0. |
| createdAt | string (date-time) |   |
| description | string | Your label for it |
| disabledAt | string (date-time) | When the service switched the endpoint off because 5 events in a row ran out of retries; absent otherwise. `PUT` with `enabled: true` clears it. |
| disabledReason | string | Why it was switched off: the count and the last delivery error |
| enabled | boolean | Whether events are delivered. `false` when you paused it, or when the service disabled it (then `disabledAt` is set). |
| events | string[] | The subscription as you set it. Absent or empty means every job-level event; per-item events are delivered only when listed here. |
| id | string | The endpoint's id, `whe_…` |
| secret | string | The signing secret in the clear, `whsec_…` — present only on the create and rotate-secret answers, so store it then. Every other read carries `secretHint` instead. |
| secretHint | string | The secret masked — its first 7 and last 4 characters — enough to tell which one your receiver holds, not enough to sign with |
| updatedAt | string (date-time) |   |
| url | string | Where events are POSTed |
