Retries and failures
If the receiver doesn't reply 2xx, the delivery is retried on a fixed schedule. If it stays silent for almost a day, the webhook is disabled and its owner gets an e-mail.
Retry schedule
| Attempt | When |
|---|---|
| 1 | immediately after the event |
| 2 | +5 seconds |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +5 hours |
| 7 | +10 hours |
| 8 | +10 hours |
That's 8 attempts over ~28 hours. Every pause carries a random jitter of ±10 %: without it, a thousand events from one transaction would hit your server in lockstep — exactly when it is already struggling.
The dispatcher wakes up once a minute. A single change arrives within a minute; a bulk operation (applying a draft, permanently deleting an employee) goes out as a batch within about 15 minutes: no more than 50 deliveries per endpoint are taken on a single pass over the queue, so one noisy receiver can't take over the whole batch. A minute-long tick makes several passes — batch after batch, for as long as its 50-second budget lasts.
The current attempt number is visible in the Smengo-Delivery-Id header, after the colon.
Response classification
| Receiver response | What we do |
|---|---|
2xx |
success, the response body is ignored |
3xx |
failure redirect_not_allowed: we don't follow redirects — the URL must be served by the final server |
410 Gone |
terminal: the delivery is marked dead and the endpoint is disabled immediately |
429, 5xx |
failure with a retry; the Retry-After header is honoured |
other 4xx |
failure with a retry |
| no response within 15 seconds | failure timeout, with a retry |
| network, TLS, DNS | failure with a retry (network_…, dns_failed) |
| the URL resolves to a private network | failure private_address — the request is not sent at all |
Retry-After
The header is understood in both RFC 9110 forms — seconds and HTTP date — and applied as max(schedule, Retry-After) capped at 1 hour. Two consequences:
- a server can postpone the next attempt but cannot speed it up:
Retry-After: 0won't turn retries into a hot loop; - it cannot postpone beyond an hour either — otherwise burning through the attempts would take weeks.
Auto-disabling
The trigger is the first delivery that exhausts all 8 attempts (or a 410 Gone response). Then:
- The endpoint is switched to the disabled state — deliveries stop.
- Everything queued for that endpoint is marked dead and will not be sent after you switch it back on: otherwise a repaired server would be hit by a 28-hour backlog.
- An e-mail goes to the owner (the organization's billing address, otherwise the endpoint creator's address).
- The webhook card shows a banner with the reason — visible even without the e-mail.
To bring delivery back: fix the receiver → Send test event → make sure you got a 2xx → switch the webhook back on. What you missed while it was off is read through the v1 API — v1 has no manual redelivery.
Pause is not the same as disabled
The Pause button differs from auto-disabling:
- while a webhook is paused, new events are not queued at all — after you resume, you get only what happens from that point on;
- deliveries already queued when you paused go out after you resume;
- a test event can be sent while paused — that's the intended way to verify a fix.
Inactive subscription
If the organization's trial has ended and the subscription is inactive, push stops along with the rest of the paid functionality: the delivery is postponed by an hour and retried, the attempt is not consumed, and the endpoint is not disabled. Once payment goes through, deliveries resume on their own within the hour. Such rows are marked subscription_inactive in the log.
Delivery log
- Kept for 30 days, together with the event body — exactly what was sent to your URL.
- Below the cards, the Recent deliveries block shows the 25 most recent deliveries across all of the organization's webhooks: time, event type, response code, attempt count, body and attempt history. The limit is shared: with three endpoints that is 25 rows in total, not 25 per endpoint.
- The first 512 bytes of the response are stored — enough to see the error text your server returned.
Codes in the "error" column
| Code | Meaning |
|---|---|
http_<code> |
the server replied non-2xx (for example http_500) |
redirect_not_allowed |
a 3xx response; redirects are not supported |
gone |
a 410 response; the endpoint was disabled |
timeout |
no response within 15 seconds |
network_<code> |
a Node network error (network_ECONNREFUSED, network_EPROTO, …) |
dns_failed |
the hostname did not resolve within the allotted time |
private_address |
the URL resolves to a private or reserved range |
not_https, port_not_allowed, userinfo_not_allowed, fragment_not_allowed, invalid_url, url_too_long, url_too_short |
the URL failed the shape checks before sending |
subscription_inactive |
the organization's subscription is inactive, the delivery is postponed by an hour |
endpoint_disabled |
the delivery was dropped when the endpoint was auto-disabled |
lease_expired |
the delivery got stuck (an invocation failure) and was picked up by a housekeeping process |
How to avoid retries
- Reply
2xxbefore doing heavy work: accept the event, verify the signature, enqueue it, reply200. - Don't reply
3xx: a load balancer redirectinghttp→httpsor/hook→/hook/breaks delivery. Register the final URL. - Return
410only when the URL is dead for good: that code disables the webhook on the first hit. - Under temporary load, reply
429or503withRetry-After— that way you control the pace without losing events.
Next
- Overview — delivery format and receiver requirements.
- Verifying the signature — ready-made Node and Python examples.