Skip to content
Webhooks

Retries & Auto-Disable

Kirimdev guarantees at-least-once delivery via an 8-attempt retry pipeline with exponential backoff and jitter, spread across a ~24-hour total window. After the 8th attempt fails, the delivery is marked failed and parks in the dead-letter queue. After 20 consecutive failed deliveries on the same subscription, the subscription is auto-disabled.

AttemptDelay before attempt
1immediate
210 s
330 s
42 m
510 m
61 h
76 h
824 h

Each delay carries ±20% jitter to avoid thundering herds on shared infrastructure. Per-attempt HTTP timeout: 10 seconds.

After attempt 8 fails, the delivery row status flips to failed and sits in the dead-letter queue until you either:

  • Replay it via the API or dashboard (see Replay below), or
  • The daily purge removes it 30 days after creation.
ResponseTreated asRetried?error_code
2xx (200, 201, 204, …)successnull
3xxpermanent failure — Kirimdev does not follow redirectsnodead on the first attempthttp_error
4xx (most)permanent failurenodead on the first attempthttp_error
4xx 408 (request timeout)transient failureyeshttp_error
4xx 429 (rate limited)transient failureyeshttp_error
5xxtransient failureyeshttp_error
DNS / TLS / connection refused / resettransient failureyesconnection_error
No response within 10 stransient failureyestimeout

The “most 4xx → dead immediately” rule reflects reality: a 401, 403, or 404 from your server almost always means a config bug (wrong URL, bad gateway auth) that retrying can’t fix. Fix the config, then replay.

Redirects are deliberately not followed. The request signature is computed against the URL you registered; following a redirect would hand a valid signature to whatever host the redirect points at. Point the subscription at the final URL instead.

Three fields, in this order:

  1. error_code — the failure class. Start here.
  2. response_status — the HTTP status, when there was one.
  3. response_body_snippet — the first 1 KB your endpoint returned.

response_status and response_body_snippet contain only what your endpoint sent. When both are null, your endpoint never answered, and error_code tells you whether that was a timeout or a connection failure.

Four error_code values mean the request never left Kirimdev, so there is nothing to debug on your side:

error_codeWhat to do
subscription_inactiveThe subscription was paused or disabled when the attempt came due. Re-enable, then replay.
subscription_deletedThe subscription was deleted before delivery. Recreate it if you still want the events.
ssrf_blockedThe URL resolves to a private or internal address. Use a publicly routable host.
no_signing_secretEvery signing secret expired. Rotate in a new one, then replay.

error_message carries a short human-readable detail (200 characters max) for whichever code applies.

  • Ack within 1-2 seconds. Persist the raw payload (and the X-Kirim-Event-Id) inside a quick DB write, then return 200. Hand off heavy processing to your own queue.

    app.post('/webhooks/kirim', async (req, res) => {
    await persistRawEvent(req.headers['x-kirim-event-id'], req.body)
    res.status(200).send('ok')
    // Async processing kicks off via your own worker.
    })
  • Return 503 if you’re overloaded rather than 200-then-drop. 503 triggers a retryable failure; you’ll get the same payload again after the backoff.

  • Never return 2xx for a payload you couldn’t store. Acking prematurely breaks the at-least-once contract on your side.

Because deliveries are at-least-once, the same event can arrive more than once — typically when a retry fires after your server processed the original but didn’t respond in time.

Dedupe on the X-Kirim-Event-Id header. The same id always represents the same logical event, regardless of attempt number or whether the delivery is a manual replay.

const fresh = await redis.set(`kirim:evt:${eventId}`, '1', { EX: 604800, NX: true })
if (!fresh) return res.status(200).send('duplicate-ack')

See Overview → Dedupe for the full pattern.

After 20 consecutive failed attempts on the same subscription, Kirimdev flips the subscription to status: 'disabled':

{
"id": "wbs_…",
"object": "webhook_subscription",
"status": "disabled",
"disabled_reason": "auto_disabled_max_consecutive_failures",
"consecutive_failures": 20,
"last_disabled_at": "2026-07-24T08:14:33Z",
"last_disabled_reason": "auto_disabled_max_consecutive_failures",
"...": "..."
}

The counter tracks attempts, not deliveries. This matters more than it sounds: a single delivery can burn up to 8 attempts on its way through the retry ladder, so an endpoint that reliably times out can trip the 20-attempt threshold after as few as three events. Any successful delivery resets the counter to zero.

disabled_reason describes the current state and is cleared when you re-enable the subscription. last_disabled_at and last_disabled_reason are history — they are never cleared, so you can still tell that a subscription was auto-disabled after recovering it, and line the outage window up against your own logs.

While disabled, no new events fan out to this subscription. New events that would have been sent are simply not enqueued — they are not buffered for later delivery, because re-enabling would otherwise trigger a thundering herd.

Deliveries whose retries were still scheduled when the subscription was disabled are terminated with error_code: "subscription_inactive". They keep their real attempt_count and the last response your endpoint gave, and they remain replayable once you re-enable.

Once your endpoint is healthy:

Terminal window
curl -X PATCH \
https://api.kirimdev.com/v1/webhook_subscriptions/wbs_… \
-H "Authorization: Bearer $KIRIM_KEY" \
-H "Content-Type: application/json" \
-d '{ "status": "active" }'

Re-enabling resets consecutive_failures to 0. Future deliveries resume immediately. Replaying old failed deliveries is opt-in — they don’t fire automatically, so a stale endpoint doesn’t drown itself the second it comes back online.

To temporarily stop deliveries without losing the subscription (e.g. during a planned maintenance window):

await kirim.webhookSubscriptions.update('wbs_…', { status: 'paused' })

Paused subscriptions don’t accumulate failed deliveries — Kirimdev drops events that would have fanned out to them. Resume with { status: 'active' }.

The dead-letter queue keeps failed deliveries for 30 days. Inspect, filter, and replay them once your endpoint is healthy again.

Terminal window
curl -G https://api.kirimdev.com/v1/webhook_deliveries \
--data-urlencode "status=failed" \
--data-urlencode "subscription_id=wbs_…" \
--data-urlencode "limit=50" \
-H "Authorization: Bearer $KIRIM_KEY"
Terminal window
curl -X POST \
https://api.kirimdev.com/v1/webhook_deliveries/wbd_…/replay \
-H "Authorization: Bearer $KIRIM_KEY"
Terminal window
curl -X POST \
https://api.kirimdev.com/v1/webhook_deliveries/bulk_replay \
-H "Authorization: Bearer $KIRIM_KEY" \
-H "Content-Type: application/json" \
-d '{
"subscription_id": "wbs_…",
"status": "failed",
"created_after": "2026-05-20T00:00:00Z",
"created_before": "2026-05-23T00:00:00Z"
}'

Cap: 1000 deliveries per bulk_replay call. If your filter matches more, the response capped field is true — paginate via created_after/created_before and call again.

  1. Spot the auto-disable via the email notification, the dashboard banner, or by polling GET /v1/webhook_subscriptions/{id} for status: 'disabled'.

  2. Fix the underlying issue. TLS cert renewal, infra rollback, bug fix — whatever the failed deliveries’ response bodies indicate.

  3. Test against a single delivery first. Pick one failed wbd_… and replay it. Check your endpoint returned 2xx.

  4. Re-enable the subscription with { status: 'active' }.

  5. Bulk-replay the backlog filtered to the outage window.

  6. Set up monitoring — alert on subscription.status != 'active' and on rising consecutive_failures so you catch the next outage before it auto-disables.

The dashboard’s Developers → Webhook Deliveries page shows every delivery (last 30 days), filterable by subscription, status, event type, and date range. Each row exposes:

  • Attempt count + last/next attempt timestamps
  • Failure reason, in plain language, derived from error_code
  • Response status + first 1 KB of the response body, shown only when your endpoint actually answered
  • Full payload (pretty-printed JSON, copy-to-clipboard)
  • Replay button

The same data is queryable via GET /v1/webhook_deliveries — see the API reference.