Webhooks API
Read the outbound delivery log and attempts, manage endpoint and receiver configs, and redeliver a delivery.
The webhooks API backs the console's Webhooks view. It exposes the outbound delivery log (with per-attempt history), lets you redeliver a delivery, and provides full CRUD for the inbound receiver and outbound endpoint configs. Signing secrets are shown once when a config is created or rotated and are never returned by any read.
The config-write routes (POST/PATCH/DELETE on endpoints and receivers, and the redeliver route)
require a secret API key. Durablex Cloud seals every signing secret at rest for you.
| Method + path | Purpose |
|---|---|
GET /webhook-deliveries | A page of outbound deliveries, newest first, plus an X-Next-Cursor header. |
GET /webhook-deliveries/{id} | One delivery with its full attempts log. |
POST /webhook-deliveries/{id}/redeliver | Re-queue a delivery for an immediate fresh attempt. |
GET /webhook-endpoints | The outbound subscription configs (no secrets). |
GET /webhook-endpoints/stats | Per-endpoint delivery health over a window. |
POST /webhook-endpoints | Create an outbound endpoint; returns the signing secret once. |
PATCH /webhook-endpoints/{id} | Edit an endpoint; optionally rotate its secret. |
DELETE /webhook-endpoints/{id} | Delete an endpoint. |
GET /webhook-receivers | The inbound receiver configs (no secrets). |
POST /webhook-receivers | Create an inbound receiver; returns the signing secret once. |
PATCH /webhook-receivers/{id} | Edit a receiver; optionally rotate its secret. |
DELETE /webhook-receivers/{id} | Delete a receiver. |
See the webhooks guide for what produces these rows. Endpoints and receivers can also be managed from the console's Webhooks view; both paths write the same rows.
Listing deliveries
GET /webhook-deliveries accepts these query parameters:
| Param | Meaning | Default |
|---|---|---|
status | One delivery status: pending, delivering, succeeded, failed, exhausted, dead. | all |
app | Restrict to deliveries for one app's runs. | all apps |
limit | Page size, 1-1000. | 100 |
cursor | Opaque keyset cursor from a previous page's X-Next-Cursor. | none |
Paging is keyset over (createdAt, id) - the same model as GET /runs:
the response carries up to limit deliveries plus an X-Next-Cursor header when more exist; the header is
absent on the last page.
curl "$DURABLEX_ENGINE_URL/webhook-deliveries?app=shop&status=exhausted&limit=20"import { createClient } from "@durablex/sdk/client";
const dx = createClient({ engineUrl: process.env.DURABLEX_ENGINE_URL! });
const page = await dx.webhooks.deliveries.list({ app: "shop", status: "exhausted", limit: 20 });
page.deliveries; // WebhookDelivery[]
page.nextCursor; // pass back as { cursor } for the next pageEach delivery has:
| Field | Meaning |
|---|---|
id | Delivery id. |
app | The app whose run produced the delivery. |
endpointId | The subscribed endpoint, or absent for a ctx.webhook.send. |
url | The destination the engine POSTs to. |
eventKind | run.succeeded, run.failed, run.cancelled, step.failed, or custom. |
sourceRunId | The run whose lifecycle produced it (absent for a custom send). |
payload | The body the engine sends. |
status | pending, delivering, succeeded, failed (awaiting retry), exhausted (retries spent), or dead (non-retryable response). |
attemptCount / maxAttempts | Attempts made / allowed. |
lastStatusCode | The latest attempt's HTTP status (absent until a code is recorded). |
nextAttemptAt | When the sweeper next retries (while failed). |
createdAt / updatedAt | RFC3339 timestamps. |
One delivery and its attempts
GET /webhook-deliveries/{id} returns the delivery above plus an attempts array - the append-only log
of every POST the sweeper made, which is the per-attempt detail the console's delivery inspector shows.
A wrong-namespace id reads back as 404.
{
"id": "9f2b...", "app": "shop", "url": "https://hooks.example/sink",
"eventKind": "run.failed", "status": "exhausted", "attemptCount": 5, "maxAttempts": 5,
"attempts": [
{ "id": "a1", "attempt": 1, "outcome": "http_error", "statusCode": 500, "responseSnippet": "boom", "durationMs": 42, "createdAt": "2026-06-25T10:00:00Z" },
{ "id": "a2", "attempt": 2, "outcome": "timeout", "durationMs": 10000, "createdAt": "2026-06-25T10:00:11Z" }
]
}| Attempt field | Meaning |
|---|---|
attempt | 1-based attempt number. |
outcome | succeeded, http_error, timeout, connection_error, or skipped. |
statusCode | The HTTP status, when the partner responded. |
responseSnippet | A bounded prefix of the response body, for debugging. |
error | The transport error, when there was no response. |
durationMs | How long the attempt took. |
curl "$DURABLEX_ENGINE_URL/webhook-deliveries/<id>"const detail = await dx.webhooks.deliveries.get("<id>");
detail.attempts; // WebhookDeliveryAttempt[]Redelivering a delivery
POST /webhook-deliveries/{id}/redeliver re-queues a delivery for an immediate fresh attempt and returns
204. It keeps the existing attempt log and grants a new retry budget, so the sweeper signs and POSTs it
again. Use it to re-send a delivery that exhausted its retries, dead-lettered on a non-retryable
response, or already succeeded (a manual re-send).
A delivery that is currently in flight (delivering) cannot be redelivered - the call returns 409 so a
manual redeliver never races the sweeper. A missing or wrong-namespace id returns 404.
curl -X POST "$DURABLEX_ENGINE_URL/webhook-deliveries/<id>/redeliver"Managing endpoints and receivers
GET /webhook-endpoints and GET /webhook-receivers return the outbound subscriptions and inbound
receivers for the namespace. Both omit the signing secret entirely - it is sealed at rest and never
leaves the engine in a read.
const endpoints = await dx.webhooks.endpoints.list(); // WebhookEndpoint[] (name?, url, scheme, eventKinds, enabled)
const receivers = await dx.webhooks.receivers.list(); // WebhookReceiver[] (name?, slug, eventName, scheme, enabled)Creating
POST /webhook-endpoints creates an outbound subscription; POST /webhook-receivers creates an inbound
receiver. The response includes the freshly generated secret once - store it now; it is never
returned again. Supplying your own secret adopts it instead of generating one.
# outbound endpoint: deliver run lifecycle events to a URL
curl -X POST "$DURABLEX_ENGINE_URL/webhook-endpoints" \
-H 'authorization: Bearer <secret-key>' -H 'content-type: application/json' \
-d '{ "name": "Acme prod", "url": "https://hooks.example/sink", "eventKinds": ["run.failed", "run.succeeded"] }'
# inbound receiver: map a verified POST /webhooks/{slug} onto a Durablex event
curl -X POST "$DURABLEX_ENGINE_URL/webhook-receivers" \
-H 'authorization: Bearer <secret-key>' -H 'content-type: application/json' \
-d '{ "name": "Stripe", "eventName": "stripe.charge" }'| Endpoint body | Meaning |
|---|---|
name | Optional label; the console falls back to the URL when absent. |
app | Optional - restrict deliveries to one app; absent means all apps. |
url | Required destination. |
eventKinds | One or more of run.succeeded, run.failed, run.cancelled, step.failed. |
secret | Optional - adopt a known secret instead of generating one. |
| Receiver body | Meaning |
|---|---|
name | Optional label; the console falls back to the slug when absent. |
app | Optional - the app the produced event belongs to. |
eventName | Required - the Durablex event a verified post is mapped to. |
targetApp | Optional - target a specific app for the produced event. |
slug | Optional - the public URL segment; generated when absent and immutable after create. |
secret | Optional - adopt a known signing secret instead of generating one. |
A caller-supplied receiver slug that is already in use returns 409 (the slug is the global inbound
routing key).
Editing, rotating, deleting
PATCH accepts a partial body - omitted fields are left unchanged. Set rotateSecret: true to issue a
new signing secret; the response then carries the new secret once (it is absent on an edit that did not
rotate). DELETE removes the config and returns 204. A wrong-namespace id returns 404.
# disable an endpoint and rotate its secret
curl -X PATCH "$DURABLEX_ENGINE_URL/webhook-endpoints/<id>" \
-H 'authorization: Bearer <secret-key>' -H 'content-type: application/json' \
-d '{ "enabled": false, "rotateSecret": true }'
curl -X DELETE "$DURABLEX_ENGINE_URL/webhook-endpoints/<id>" -H 'authorization: Bearer <secret-key>'Endpoint delivery stats
GET /webhook-endpoints/stats rolls up the delivery log per endpoint so the console can show delivery
health without a stored health field. An optional since (RFC3339) bounds the window; absent, it
defaults to the last 30 days.
| Field | Meaning |
|---|---|
endpointId | The endpoint the row aggregates. |
delivered | Total deliveries in the window (including in-flight). |
succeeded | Deliveries that settled successfully. |
failed | Deliveries that terminally failed (exhausted or dead). |
lastDelivery | The most recent delivery's timestamp. |
curl "$DURABLEX_ENGINE_URL/webhook-endpoints/stats?since=2026-06-01T00:00:00Z"