Delivery & retries
Delivery guarantees
Delivery is at-least-once. Slothbox attaches a stable webhook-id to each
event and reuses it verbatim across every retry, so your endpoint may see the
same event more than once. Dedupe on webhook-id
to make handling idempotent.
Respond 2xx within 20 seconds — each attempt has a hard 20-second deadline
covering connect, TLS, and the response. Any non-2xx response, a timeout, or a
connection error counts as a failed attempt.
Retries
Failed deliveries are retried with an escalating backoff over roughly 8–9
hours. The waits before each retry are 30s, 2m, 10m, 30m, 1h, 2h, 5h —
each jittered down by up to half — for eight attempts in total; after the last
one the delivery is marked exhausted. (Endpoints with
ordered delivery retry on a shorter cadence.) Across
every attempt the webhook-id stays the same; the webhook-timestamp header is
re-stamped each time, so keep any timestamp tolerance relative to that header.
Auto-disable
Every delivery that exhausts all of its retries bumps the endpoint's
consecutive-failure count; any successful delivery resets it. When the count
reaches 5 and the failing streak has lasted at least 24 hours
(measured from the streak's first exhausted delivery — so a brief outage, even
one that exhausts a few deliveries, won't disable a healthy endpoint), Slothbox
disables the endpoint and stops delivering to it, including retries already in
flight. The org owners are emailed. Slothbox also
emits a webhook.endpoint.disabled event,
which fans out to any other enabled endpoint in your org that subscribes to it
(or to *) — the just-disabled endpoint no longer receives anything. Re-enable a
disabled endpoint from Settings → Webhooks (or by PATCH-ing it with
{ "status": "enabled" }).
Replay
Every past delivery is kept in the delivery log. To replay one over the API, list
the deliveries for the endpoint to find its deliveryId:
curl https://api.slothbox.dev/organizations/org_123/webhooks/whe_01HXYZ/deliveries \
-H "Authorization: sk_..."
{
"deliveries": [
{ "deliveryId": "msg_01HXYZ", "eventType": "environment.started", "status": "exhausted", "attemptCount": 8 }
],
"cursor": "..."
}
Then redeliver it (or replay any delivery straight from Settings → Webhooks):
curl -X POST \
https://api.slothbox.dev/organizations/org_123/webhooks/whe_01HXYZ/deliveries/msg_01HXYZ/redeliver \
-H "Authorization: sk_..."
{ "webhookId": "msg_NEW01HXYZ" }
A replay re-sends the original event's type and data as a brand-new
delivery — with a fresh webhook-id and webhook-timestamp, not the original
ones (the response above is that new id). A consumer that de-duplicates on
webhook-id will therefore treat a replay as a new event and process it again,
which is usually what you want when replaying a delivery your endpoint missed or
mishandled.
Rotating the signing secret
Rotate an endpoint's signing secret from Settings → Webhooks, or over the API:
curl -X POST \
https://api.slothbox.dev/organizations/org_123/webhooks/whe_01HXYZ/secret/roll \
-H "Authorization: sk_..."
{ "secret": "whsec_...", "overlapSeconds": 86400 }
By default the old secret keeps verifying for 24 hours (the overlapSeconds
above), during which deliveries are signed with both secrets (a space-delimited
list in the webhook-signature header). Deploy the new secret within that window
and you can roll without downtime — see
rule 5 of verifying.
To cut over immediately instead — invalidating the old secret right away
(overlapSeconds of 0) — pass { "expireOld": true } in the request body. Use
this only if you suspect the old secret is compromised.
Ordered delivery (optional)
By default, deliveries are not ordered — because each event retries independently, a failed event can land after later ones that succeeded first. If you need strict in-order delivery per endpoint, enable Ordered delivery on the endpoint. Two things to know:
- Ordering is per-endpoint and reflects the order Slothbox committed the underlying changes. It is not a global or cross-resource total order.
- A stuck delivery holds up later ones for that endpoint until it succeeds or
exhausts its retries. To keep that head-of-line blocking short, ordered
endpoints retry on a short fixed cadence (about every 3 minutes, with the
same eight-attempt cap) instead of the multi-hour curve — a failing
ordered delivery is marked
exhaustedafter roughly 20 minutes, not 9 hours.
Most integrations don't need this; timestamp plus idempotency on webhook-id
is usually enough.