Deliveries, retries and replay
A delivery is one event sent to one endpoint. What counts as delivered, how a failure is retried for about an hour, and how to send a delivery again.
A delivery is one event on its way to one endpoint. When your endpoint answers 2xx, the delivery is done. When it does not, Infrared tries again: 8 attempts over about an hour. Every attempt is recorded, and any delivery can be sent again.
List the deliveries of an endpoint to see how each one went:
curl https://api.useinfrared.com/v1/webhooks/whe_0dFoExHEccOjnJ7ZHHwXsq/deliveries \
-H "Authorization: Bearer $INFRARED_KEY"const response = await fetch("https://api.useinfrared.com/v1/webhooks/whe_0dFoExHEccOjnJ7ZHHwXsq/deliveries", {
headers: { Authorization: `Bearer ${process.env.INFRARED_KEY}` },
});
const deliveries = await response.json();import os
import requests
response = requests.get(
"https://api.useinfrared.com/v1/webhooks/whe_0dFoExHEccOjnJ7ZHHwXsq/deliveries",
headers={"Authorization": f"Bearer {os.environ['INFRARED_KEY']}"},
)
deliveries = response.json(){"object": "list","data": [{"object": "webhook_delivery","id": "whd_0oBGD4cLwRR1dYwd5ow6oW","endpoint_id": "whe_0dFoExHEccOjnJ7ZHHwXsq","event_id": "evt_2uhhIo2wErQojIHSPaTfHE","event_type": "moderation.completed","status": "pending","attempts": 3,"response_status": null,"error": "getaddrinfo ENOTFOUND hooks.example.com","duration_ms": 5,"next_attempt_at": "2026-10-08T08:57:13.217Z","delivered_at": null,"replayed_from_id": null,"attempt_log": [{ "at": "2026-10-08T08:53:42.142Z", "error": "getaddrinfo ENOTFOUND hooks.example.com", "status": null, "duration_ms": 7 },{ "at": "2026-10-08T08:54:12.740Z", "error": "getaddrinfo ENOTFOUND hooks.example.com", "status": null, "duration_ms": 7 },{ "at": "2026-10-08T08:55:13.217Z", "error": "getaddrinfo ENOTFOUND hooks.example.com", "status": null, "duration_ms": 5 }],"created_at": "2026-10-08T08:53:41.692Z"}],"has_more": false,"next_cursor": null}This delivery has failed three times, because the endpoint's host name does not resolve. It is still pending: the fourth attempt is due at next_attempt_at, two minutes after the third.
- Attempt 1Since the first attempt: 0:00
- Before it: 30 sAttempt 2Since the first attempt: 0:30
- Before it: 1 minAttempt 3Since the first attempt: 1:30
- Before it: 2 minAttempt 4Since the first attempt: 3:30
- Before it: 4 minAttempt 5Since the first attempt: 7:30
- Before it: 8 minAttempt 6Since the first attempt: 15:30
- Before it: 16 minAttempt 7Since the first attempt: 31:30
- Before it: 32 minAttempt 8Since the first attempt: 1:03:30
2xxFrom any attemptDelivered:
succeeded4xxOther than408and429, from any attemptNot delivered:
failed- After attempt 8All 8 attempts failed
Not delivered:
exhausted
50 failed attempts in a row, across all deliveries, disable the endpoint.
What counts as delivered
Your endpoint has 10 seconds to answer, and only a status from 200 to 299 counts. Infrared does not read the body of the answer, but an answer larger than 64 KB fails the attempt.
| Your endpoint answers | The delivery |
|---|---|
| 2xx | Is succeeded. No more attempts. |
| 4xx, except 408 and 429 | Is failed at once. No retry. |
| 408, 429, any 5xx | Is retried. |
| 3xx | Is retried. Redirects are not followed. |
| Nothing within 10 seconds | Is retried. |
| A connection, DNS or TLS error | Is retried. |
So answer 4xx only to a request you will never accept, such as one with a bad signature. For anything that will pass, such as a deploy in progress or a database that is briefly away, answer 5xx and let the retry do its work.
Answer first, then do the work. A handler that takes longer than 10 seconds is counted as failed even when it finishes, and the same event arrives again.
The retry schedule
A delivery gets up to 8 attempts. The wait before a retry starts at 30 seconds and doubles each time.
| Attempt | Wait before it | Since the first attempt |
|---|---|---|
| 1 | None | 0 |
| 2 | 30 seconds | 30 seconds |
| 3 | 1 minute | 1 minute 30 seconds |
| 4 | 2 minutes | 3 minutes 30 seconds |
| 5 | 4 minutes | 7 minutes 30 seconds |
| 6 | 8 minutes | 15 minutes 30 seconds |
| 7 | 16 minutes | 31 minutes 30 seconds |
| 8 | 32 minutes | 1 hour 3 minutes 30 seconds |
A retry sends the same body with the same Infrared-Delivery header and a new signature. A Retry-After header in your answer is not read.
The Deliveries section of the dashboard says that retries run for about a day. The schedule above is what runs.
Delivery statuses
status | Meaning | In the dashboard |
|---|---|---|
pending | Waiting for its first attempt or for a retry. next_attempt_at says when. | Pending |
succeeded | Your endpoint answered 2xx. delivered_at is set. | Delivered |
failed | It ended early: a 4xx that is not retried, or the endpoint was disabled before the next attempt. | Failed |
exhausted | All 8 attempts failed. | Exhausted |
response_status, error and duration_ms describe the last attempt. attempt_log holds the last 20, oldest first. In an entry, error is set when no answer arrived. When your endpoint did answer, the entry carries the status code, and the delivery's own error reads HTTP 503.
When an endpoint keeps failing
Each failed attempt adds one to the endpoint's consecutive_failures, and any 2xx sets it back to 0. At 50, Infrared disables the endpoint and emails the organisation's Owners and Admins.
- The count is of attempts, across all events. One event that fails every attempt adds 8. An endpoint that is down while 50 events happen is disabled within seconds.
- Events that happen while the endpoint is disabled are not sent to it later. No delivery is created for them, so there is nothing to replay. Read them from the event log.
- Deliveries that were waiting for a retry end as
failed, with the errorendpoint disabled. Those can be replayed. - Enable the endpoint again in the dashboard, or with
PATCH /v1/webhooks/{id}and"status": "active". The count starts again at 0.
The button in the email does not open the endpoint today. Open Webhooks and pick the endpoint yourself.
Replay a delivery
A replay sends the same event to the same endpoint again, as a new delivery with all 8 attempts.
curl -X POST https://api.useinfrared.com/v1/webhooks/deliveries/whd_0oBGD4cLwRR1dYwd5ow6oW/replay \
-H "Authorization: Bearer $INFRARED_KEY"const response = await fetch("https://api.useinfrared.com/v1/webhooks/deliveries/whd_0oBGD4cLwRR1dYwd5ow6oW/replay", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.INFRARED_KEY}` },
});
const delivery = await response.json();import os
import requests
response = requests.post(
"https://api.useinfrared.com/v1/webhooks/deliveries/whd_0oBGD4cLwRR1dYwd5ow6oW/replay",
headers={"Authorization": f"Bearer {os.environ['INFRARED_KEY']}"},
)
delivery = response.json(){
"object": "webhook_delivery",
"id": "whd_6cnh5GbkPAb7B6vosVu8o0",
"endpoint_id": "whe_0dFoExHEccOjnJ7ZHHwXsq",
"event_id": "evt_2uhhIo2wErQojIHSPaTfHE",
"event_type": "moderation.completed",
"status": "pending",
"attempts": 0,
"response_status": null,
"error": null,
"duration_ms": null,
"next_attempt_at": "2026-10-08T08:55:23.355Z",
"delivered_at": null,
"replayed_from_id": "whd_0oBGD4cLwRR1dYwd5ow6oW",
"attempt_log": [],
"created_at": "2026-10-08T08:55:23.355Z"
}- The new delivery has its own id.
replayed_from_idnames the one it repeats. - The event is unchanged: the same
idin the body, the same payload. - It goes to the endpoint's current URL and is signed with its current secret.
- Any delivery can be replayed, whatever its status. A replay to a disabled endpoint ends
failed.
In the dashboard, Replay is on each row of an endpoint's Deliveries.
Handle an event once
Delivery is at least once. A retry after a slow answer and a replay both bring an event your handler may have processed already, so make the handler safe to run twice.
- Deduplicate on the event's
id, theevt_value in the body. It is the same on every retry, on a replay, and on every endpoint that receives the event. Infrared-Deliveryis narrower. It stays the same across the automatic retries of one delivery, and changes on a replay and from one endpoint to another.- Keep the ids you have processed for 30 days. An event cannot be sent after that.
- Do not rely on order. Deliveries run side by side, and a retried event arrives after later ones. Compare the events'
created_at, or read the current state from the API.
The delivery log
Deliveries are kept for 30 days. List deliveries returns those of one endpoint, newest first, with a status filter and the usual limit and cursor. Deleting an endpoint deletes its deliveries.
On an endpoint's page in the dashboard, Deliveries shows the same list 25 rows at a time: the event, the status, the response code, the attempts, the duration and when it was created. Filter by All, Pending, Delivered, Failed or Exhausted, and hover a response code to read the error. The attempt log is in the API only.
Listing deliveries needs the read scope and a replay needs write. In the dashboard, replaying needs the Developer role or one above it.
Next
- Verify signaturesEvery delivery is signed with the endpoint's secret. Check the signature and its timestamp before you trust the body, with code you can copy.
- Event typesThe twelve events Infrared sends, when each one fires and what its data holds, with a real payload for moderations, queue items, authors and actions.
- Going liveWhat to settle before production traffic depends on Infrared, from keys and retries to the review queue. Each item links to the page behind it.
Something missing or unclear on this page? Write to support@useinfrared.com.