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, and what they are for — with the calls for every SDK, the CLI and MCP — on Webhooks. * marks a required field.
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
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 |
|
| 422 |
|
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 (required) | 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 |
|
Update a webhook endpoint
PUT /api/v1/webhook-endpoints/{id}
Accepts an Idempotency-Key
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: falsepauses the endpoint: nothing new is queued for it, and events already queued are closed asFAILED(endpoint disabled) rather than held.enabled: trueturns it back on and also clears an automatic disable —disabledAt,disabledReasonandconsecutiveFailures.POST /{id}/testis the cheap way to check the receiver first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
| id (required) | 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 |
|
| 404 |
|
Delete a webhook endpoint
DELETE /api/v1/webhook-endpoints/{id}
Accepts an Idempotency-Key
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 (required) | 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 |
|
Recent deliveries to an endpoint
GET /api/v1/webhook-endpoints/{id}/deliveries
Paged — 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 (required) | 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 |
|
Rotate the signing secret
POST /api/v1/webhook-endpoints/{id}/rotate-secret
Accepts an Idempotency-Key
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 (required) | 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 |
|
Send a test delivery
POST /api/v1/webhook-endpoints/{id}/test
Accepts an Idempotency-Key
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
2xxresets 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 (required) | 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 |
|
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 listingse.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.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.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 |