Skip to main content
The webhooks API backs the console’s Webhooks view. It exposes both the outbound delivery log (with per-attempt history) and the inbound source delivery log, lets you redeliver an outbound delivery or replay an inbound one, and provides full CRUD for the inbound source 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 sources) and the redeliver and replay routes require a secret API key. Duraton seals every signing secret at rest for you. See the webhooks guide for what produces these rows. Endpoints and sources 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: 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.
Each delivery has:

One delivery and its attempts

GET /webhook-deliveries/{id} returns the delivery above plus an attempts array - the append-only log of every POST Duraton made, which is the per-attempt detail the console’s delivery inspector shows. A wrong-project id reads back as 404.

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 Duraton 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 an in-flight attempt. A missing or wrong-project id returns 404.

The inbound delivery log

Every POST to a source’s receive URL is recorded as an inbound source delivery, alongside its admission outcome - the received-side counterpart to the outbound delivery log above. A verified delivery can be replayed to re-ingest its stored body. GET /webhook-source-deliveries accepts these query parameters, all optional; omit them for a workspace-wide listing: Paging is keyset, the same model as GET /webhook-deliveries: the response carries up to limit deliveries plus an X-Next-Cursor header when more exist. A status outside the accepted set (ingested, deduped, unauthorized, invalid, too_large, misconfigured) returns 400 naming the accepted values, rather than an empty page - so a typo is a loud error, not a silently empty result.
Each delivery’s status is the original admission outcome of that POST, frozen at ingest - a later replay does not rewrite it (per-replay outcomes live in attempts[]):

One inbound delivery and its attempts

GET /webhook-source-deliveries/{id} returns the delivery with its request headers, the stored body (present only for a delivery that verified), and an append-only attempts log. Attempt 1 is the initial ingest (trigger: initial); each manual replay appends an attempt (trigger: replay) recording the actor who triggered it. A wrong-project id reads back as 404.
The row and each attempt carry eventId (the event the admission emitted) and runId (a run it woke), so you can pivot from a delivery to its event to a run. Both are omitted when the admission produced neither - a deduped or rejected post, or an ingest that matched no workflow - as on the replayed attempt above. The delivery’s top-level status is the original admission outcome and is frozen: a replay never rewrites it, so ?status=ingested still returns a delivery after it has been replayed. Each replay’s own outcome lives only in its attempts[] row - the attempt above ingested first, then deduped on replay, while the delivery stays ingested.

Replaying an inbound delivery

POST /webhook-source-deliveries/{id}/replay re-ingests the stored body and returns 200 with the replay outcome - what re-ingesting the body produced - as { deliveryId, status, eventId?, runId? }. It requires a full-access key. Only a delivery that originally verified (ingested or deduped) is replayable - a rejected delivery stored no verified body. The replay outcome is recorded as a new attempts[] row on the delivery; the delivery’s top-level status stays the original admission outcome. Replay does not re-check the signature (the delivery was verified when it arrived, and its signed timestamp would now be far outside the tolerance window). It re-runs the source’s current dedupeKey and event mapping over the stored body, so within a dedupe window a replay dedupes exactly as a real provider redelivery would. See the guide for the replay semantics, including that a replay re-triggers downstream workflow effects.
The inbound source-delivery routes are exposed by the TypeScript SDK’s duraton.webhooks.sourceDeliveries surface (list, listAll, get, replay).

Managing endpoints and sources

GET /webhook-endpoints and GET /webhook-sources return the outbound subscriptions and inbound sources for the project. Both omit the signing secret entirely - it is sealed at rest and never leaves Duraton in a read.

Subscribable event kinds

An endpoint’s eventKinds is the set of lifecycle kinds it subscribes to. Delivery is subscription-gated: an endpoint only receives a kind it explicitly subscribed to, so a payload never arrives for a kind you did not ask for. Every kind fires on a terminal transition - a run reaching a final state, or a step settling for the last time. The step.* kinds are per-step: subscribe to them to track a run’s progress step by step instead of waiting for the whole run to finish. They fire only on a step’s terminal transition (not on intermediate retries). custom is not subscribable - it is produced by a workflow’s ctx.webhook.send, not by subscribing an endpoint.

Creating

POST /webhook-endpoints creates an outbound subscription; POST /webhook-sources creates an inbound source. 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.
A duplicate endpoint url for the same app, or a duplicate endpoint name within the project, returns 409. The receive URL is not an input: Duraton always issues a 128-bit random, unguessable token for it (immutable after create) and returns the full URL as receiveUrl on the created source. A duplicate source name within the project returns 409.

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-project id returns 404.

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.