Skip to content

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"
200 OKJSON
{"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.

The retry schedule
  1. Attempt 1Since the first attempt: 0:00
  2. Before it: 30 sAttempt 2Since the first attempt: 0:30
  3. Before it: 1 minAttempt 3Since the first attempt: 1:30
  4. Before it: 2 minAttempt 4Since the first attempt: 3:30
  5. Before it: 4 minAttempt 5Since the first attempt: 7:30
  6. Before it: 8 minAttempt 6Since the first attempt: 15:30
  7. Before it: 16 minAttempt 7Since the first attempt: 31:30
  8. Before it: 32 minAttempt 8Since the first attempt: 1:03:30
How it ends
  1. 2xxFrom any attempt

    Delivered:succeeded

  2. 4xxOther than 408 and 429, from any attempt

    Not delivered:failed

  3. After attempt 8All 8 attempts failed

    Not delivered:exhausted

50 failed attempts in a row, across all deliveries, disable the endpoint.

The attempts of one delivery, the wait before each, and how it ends.

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 answersThe delivery
2xxIs succeeded. No more attempts.
4xx, except 408 and 429Is failed at once. No retry.
408, 429, any 5xxIs retried.
3xxIs retried. Redirects are not followed.
Nothing within 10 secondsIs retried.
A connection, DNS or TLS errorIs 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.

AttemptWait before itSince the first attempt
1None0
230 seconds30 seconds
31 minute1 minute 30 seconds
42 minutes3 minutes 30 seconds
54 minutes7 minutes 30 seconds
68 minutes15 minutes 30 seconds
716 minutes31 minutes 30 seconds
832 minutes1 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

statusMeaningIn the dashboard
pendingWaiting for its first attempt or for a retry. next_attempt_at says when.Pending
succeededYour endpoint answered 2xx. delivered_at is set.Delivered
failedIt ended early: a 4xx that is not retried, or the endpoint was disabled before the next attempt.Failed
exhaustedAll 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 error endpoint 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"
202 AcceptedJSON
{
  "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_id names the one it repeats.
  • The event is unchanged: the same id in 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, the evt_ value in the body. It is the same on every retry, on a replay, and on every endpoint that receives the event.
  • Infrared-Delivery is 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

Something missing or unclear on this page? Write to support@useinfrared.com.