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.
| Attempt | When |
|---|---|
| 1 | Immediately |
| 2 | 1 minute after attempt 1 fails |
| 3 | 5 minutes after attempt 2 |
| 4 | 30 minutes after attempt 3 |
| 5 | 2 hours after attempt 4 |
| 6 | 6 hours after attempt 5 |
| 7 | 12 hours after attempt 6 |
| 8 | 24 hours after attempt 7 |
| 9 | 24 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-Idis the same on every attempt of one delivery. So is the body'swebhookId.Signals-Delivery-Attemptcounts1to9.- The body holds the same run, counts and tokens, with a new
sentAt. Signals-Signatureis signed fresh on each attempt, with a new timestamp.
How Signals reads your answer
| Your answer | What Signals does |
|---|---|
2xx | Delivered. Done. |
410 Gone | Stops 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-After | Waits 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 4xx | Retried on the schedule. Often a failed signature check or a wrong path. |
5xx, 408, timeout, network error | Retried on the schedule. |
3xx | Retried on the schedule. Redirects are never followed. |
| Blocked URL | Dead 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 app | Kind | What happened | What to do |
|---|---|---|---|
| HTTP error | http_status | Your server answered 5xx, 408, 413 or 429. The status code is shown. | Read your logs. |
| Rejected by your server (4xx) | client_error | Your 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) | gone | Your server answered 410. The webhook is disabled. | Fix the endpoint, then Re-enable. |
| Timed out | timeout | No answer within 10 seconds. | Answer first, work later. |
| Network error | network | Signals could not connect: DNS failure, refused connection, TLS error. | Check the DNS record, the certificate and your firewall. |
| Redirect (not followed) | redirect | Your server answered 3xx. | Save the final URL in the webhook. Watch for http to https and www redirects. |
| URL blocked | unsafe_url | At send time the hostname resolved to a private or internal address. | Use a public hostname. |
| Waiting for the URL to recover | endpoint_unavailable | A 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 exists | event_missing | The run's events are gone, for example because their project was deleted. | Nothing to do. |
| Webhook was paused | paused | The webhook was paused while this delivery was in progress. | It is sent after you resume. |
Status labels
| Status | Meaning |
|---|---|
| Pending | Waiting for its next attempt. Also while the webhook is paused. |
| Sending | In flight now. |
| Delivered | Your endpoint returned 2xx. |
| Failed | Ended without a 2xx outside the schedule, for example because the run's events are gone. Can be replayed. |
| Dead | All 9 attempts failed, or your server answered 410, or the URL is blocked. Can be replayed. |
| Replayed | A 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 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:
| Key | Where | Catches |
|---|---|---|
webhookId | The body and the Signals-Webhook-Id header | Retries of one delivery. Skip the whole POST. |
eventId | Each item of a page | Everything else: replays, a page read twice, a crash halfway through the loop. |
- Store each
webhookIdyou handled and skip a POST you have seen. - Store each
eventIdwith a unique index, not an in-memory set. Skip events you already have. - Return
200once the body is stored. A4xxor5xxmakes Signals send the POST again. - Do side effects (create a CRM note, post to Slack) after the
eventIdcheck, in the same transaction if you can.
The full loop is on Read a run's events.