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 codes
Section titled “Error codes”error_code | What happened |
|---|---|
http_error | Your endpoint answered with a non-2xx status. response_status holds it. |
timeout | Your endpoint accepted the connection but did not answer within 10 seconds. |
connection_error | We could not reach your endpoint at all — DNS, firewall, TLS, or a reset socket. |
subscription_inactive | Not sent. The subscription was paused or disabled when the attempt came due. |
subscription_deleted | Not sent. The subscription was deleted before the event could be delivered. |
ssrf_blocked | Not sent. The target URL resolves to a private or internal address. |
no_signing_secret | Not 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.
SDK Changed
Section titled “SDK ”@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}Dashboard Changed
Section titled “Dashboard ”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 →