SteveSignalsDocs

Webhooks

Retries and failures

What counts as success, the nine-attempt retry schedule, Retry-After, 410, dead deliveries, replay, auto-disable and how to make your endpoint idempotent.

Signals retries a failed POST on a fixed schedule for about 3 days. When the schedule runs out, the delivery is Dead. Nothing is lost: you can replay it from the app or the API. This page explains each step.

What counts as success

A delivery succeeds when your endpoint returns any 2xx status within 10 seconds.

Everything else is a failure: a 4xx or 5xx, a 3xx redirect, a timeout, a connection error. Signals reads at most 4 KB of your response and ignores it.

Return 200 as soon as you have stored the body. Read the pages afterwards, in a background job. A slow handler that times out gets the same POST again.

Retry schedule

A delivery gets up to 9 attempts. The wait counts from the previous failure. Each wait varies by ±10 %, so many endpoints do not retry at the same second.

AttemptWhen
1Immediately
21 minute after attempt 1 fails
35 minutes after attempt 2
430 minutes after attempt 3
52 hours after attempt 4
66 hours after attempt 5
712 hours after attempt 6
824 hours after attempt 7
924 hours after attempt 8

Total span: about 2.9 days. After a failed attempt 9 the delivery is Dead. It stays in the log, and you can replay it.

Headers on each attempt:

  • Signals-Webhook-Id is the same on every attempt of one delivery. So is the body's webhookId.
  • Signals-Delivery-Attempt counts 1 to 9.
  • The body holds the same run, counts and tokens, with a new sentAt.
  • Signals-Signature is signed fresh on each attempt, with a new timestamp.

How Signals reads your answer

Your answerWhat Signals does
2xxDelivered. Done.
410 GoneStops at once. The delivery is Dead, and the webhook is Disabled with the banner "Your server answered 410 Gone, so Signals stopped sending to this URL. Fix your server, then re-enable it."
429 or 503 with Retry-AfterWaits for the scheduled retry or for Retry-After (at most 1 hour), whichever is later. A short Retry-After never makes Signals retry sooner.
Any other 4xxRetried on the schedule. Often a failed signature check or a wrong path.
5xx, 408, timeout, network errorRetried on the schedule.
3xxRetried on the schedule. Redirects are never followed.
Blocked URLDead at once. The hostname resolved to a private or internal address.

The URL recovery pause

When one delivery to a URL fails, Signals stops calling that URL for the other waiting deliveries until the next attempt time. They show Waiting for the URL to recover. No request is made, and these skipped tries do not use up an attempt. A single success reopens the URL.

Failure kinds

The Deliveries sheet shows one row per run and one label per failed delivery.

Label in the appKindWhat happenedWhat to do
HTTP errorhttp_statusYour server answered 5xx, 408, 413 or 429. The status code is shown.Read your logs.
Rejected by your server (4xx)client_errorYour server answered another 4xx, for example 400, 401 or 404.Check the signature code, the path and any auth in front of it.
URL gone (410)goneYour server answered 410. The webhook is disabled.Fix the endpoint, then Re-enable.
Timed outtimeoutNo answer within 10 seconds.Answer first, work later.
Network errornetworkSignals could not connect: DNS failure, refused connection, TLS error.Check the DNS record, the certificate and your firewall.
Redirect (not followed)redirectYour server answered 3xx.Save the final URL in the webhook. Watch for http to https and www redirects.
URL blockedunsafe_urlAt send time the hostname resolved to a private or internal address.Use a public hostname.
Waiting for the URL to recoverendpoint_unavailableA recent delivery to this URL failed, so Signals is pausing calls to it. No request was made and no attempt was used.Fix the endpoint. A success clears it.
Event no longer existsevent_missingThe run's events are gone, for example because their project was deleted.Nothing to do.
Webhook was pausedpausedThe webhook was paused while this delivery was in progress.It is sent after you resume.

Status labels

StatusMeaning
PendingWaiting for its next attempt. Also while the webhook is paused.
SendingIn flight now.
DeliveredYour endpoint returned 2xx.
FailedEnded without a 2xx outside the schedule, for example because the run's events are gone. Can be replayed.
DeadAll 9 attempts failed, or your server answered 410, or the URL is blocked. Can be replayed.
ReplayedA replay replaced this delivery with a new one.

Dead deliveries

A dead delivery keeps its run and its counts. Signals does not send it again by itself. You choose when: replay it, or re-enable a disabled webhook, which replays its dead deliveries for you.

Auto-disable

Signals sets a webhook to Disabled and stops sending in two cases. Test deliveries do not count.

  • No success for 3 days. The banner reads: "Signals stopped sending to this URL after 3 days without a successful delivery. Fix your server, then re-enable it."
  • Your server answered 410. At once, see above.

If the Steve team turned the webhook off, the banner reads: "The Steve team turned this webhook off. Re-enable it when you are ready."

Fix the endpoint, then click Re-enable. Signals continues where it stopped: runs that finished while the webhook was disabled are sent, oldest first, and dead deliveries are queued again. With the management API you can send resume: "now" instead, to skip what happened in between.

Replay

Open View deliveries from the row menu.

  • Resend on a failed row retries the same delivery. It keeps its webhookId, and the attempt counter starts again. The row does not turn Replayed.
  • Replay on a dead row sends the run as a new delivery. Test deliveries have no Replay button.
  • Replay dead at the top of the sheet replays every dead delivery of the webhook. It does not touch failed rows.
  • The API replays too, and can also pick by run, by time or failed status: POST /api/public/v1/webhooks/{id}/replay. See Test and manage.
  • Resend and replay need an active webhook.

A replay is a new delivery. It has a new webhookId, the attempt counter starts at 1, and its page tokens are fresh, valid for another 30 days. The old row turns Replayed. The events are the same, with the same eventIds.

The Deliveries sheet
The Deliveries sheet. A dead row shows its error label and a Replay button.

The log keeps deliveries for 30 days, then deletes them.

Make your endpoint idempotent

The same POST can reach you more than once, after a timeout your server did process or after a pause that landed mid-request. A replay sends the same run with a new webhookId. A Resend keeps the webhookId. Use two keys:

KeyWhereCatches
webhookIdThe body and the Signals-Webhook-Id headerRetries of one delivery. Skip the whole POST.
eventIdEach item of a pageEverything else: replays, a page read twice, a crash halfway through the loop.
  • Store each webhookId you handled and skip a POST you have seen.
  • Store each eventId with a unique index, not an in-memory set. Skip events you already have.
  • Return 200 once the body is stored. A 4xx or 5xx makes Signals send the POST again.
  • Do side effects (create a CRM note, post to Slack) after the eventId check, in the same transaction if you can.

The full loop is on Read a run's events.