Skip to content

Webhook deliveries now tell you why they failed

Released: July 26, 2026

Delivery failures are no longer ambiguous Added

Section titled “Delivery failures are no longer ambiguous ”

A dead webhook delivery used to give you a status code and, sometimes, a snippet of a response body. When the failure had nothing to do with an HTTP response — a timeout, a refused connection, or us declining to send at all — you got nothing useful. Worse, our own internal reason was written into response_body_snippet and displayed under the label “Response body”, so a message like Subscription is disabled looked like something your own server had said.

Every delivery object now carries two new fields:

{
"id": "wbd_...",
"object": "webhook_delivery",
"status": "dead",
"attempt_count": 6,
"response_status": null,
"response_body_snippet": null,
"error_code": "timeout",
"error_message": "The operation was aborted due to timeout",
...
}

response_status and response_body_snippet now mean exactly one thing: what your endpoint returned. If they are null, your endpoint never answered — and error_code says why.

error_codeWhat happened
http_errorYour endpoint answered with a non-2xx status. response_status holds it.
timeoutYour endpoint accepted the connection but did not answer within 10 seconds.
connection_errorWe could not reach your endpoint at all — DNS, firewall, TLS, or a reset socket.
subscription_inactiveNot sent. The subscription was paused or disabled when the attempt came due.
subscription_deletedNot sent. The subscription was deleted before the event could be delivered.
ssrf_blockedNot sent. The target URL resolves to a private or internal address.
no_signing_secretNot sent. Every signing secret on the subscription had expired.

The first three mean your endpoint is involved. The rest mean the request never left our side, so there is nothing to debug on yours — fix the subscription and replay.

error_code is null on a successful delivery, including one that succeeded after earlier attempts failed.

Retry history is no longer destroyed Fixed

Section titled “Retry history is no longer destroyed ”

When a delivery was terminated for an internal reason — most commonly because the subscription had been auto-disabled while its retries were still scheduled — we rewrote the row as if it had never been sent: attempt_count reset to 0, and response_status, duration_ms and response_body_snippet cleared.

That is fixed. Terminating a delivery now preserves the real attempt_count and whatever the last genuine attempt recorded. A delivery that failed six times against your endpoint before the subscription was disabled now reports attempt_count: 6 and the status your server actually returned, alongside error_code: "subscription_inactive".

Rows written before this release are not repaired — the data was not stored. Deliveries created from now on are accurate.

Auto-disable is now traceable Added

Section titled “Auto-disable is now traceable ”

Webhook subscription objects gain last_disabled_at and last_disabled_reason:

{
"id": "wbs_...",
"object": "webhook_subscription",
"status": "active",
"disabled_reason": null,
"last_disabled_at": "2026-07-24T08:14:33Z",
"last_disabled_reason": "auto_disabled_max_consecutive_failures",
...
}

disabled_reason describes the current state and is cleared when you re-enable a subscription. The last_disabled_* pair is history and is never cleared, so you can still tell that a subscription was auto-disabled after you have recovered it — and correlate the outage window with your own logs.

A reminder of the policy behind it: 20 consecutive failed attempts (not deliveries) flip a subscription to disabled. Because one delivery can burn up to 8 attempts, an endpoint that reliably times out can trip the threshold in as few as three events.

@kirimdev/sdk@3.15.0 picks up all four fields through the refreshed openapi.json snapshot. No method signatures changed, so upgrading is a version bump and nothing else.

One thing to know if you build WebhookDelivery objects by hand — test fixtures, mocks — error_code and error_message are required on the response type, so those literals need the two new keys.

const delivery = await kirim.webhookDeliveries.retrieve('wbd_...')
if (delivery.error_code === 'timeout') {
// your endpoint was too slow — check the handler, not the payload
} else if (delivery.error_code === 'subscription_inactive') {
// nothing was sent; re-enable the subscription, then replay
}

Developers → Deliveries shows a dedicated Failure reason block with plain-language copy for each error code, kept separate from the response body. The list view falls back to the error code in the Resp column when there is no HTTP status, so failures are readable without opening every row.

Read about retries and the DLQ →