WebhooksRetries and failures

Retries and failures

The schedule

A delivery that does not answer 2xx is retried on a fixed schedule. Delays are measured from the attempt that just failed:

After attemptNext attempt in
1stimmediately
2nd1 minute
3rd5 minutes
4th30 minutes
5th2 hours
6th6 hours
7th12 hours
8th24 hours
9th— dead-lettered

Nine attempts over roughly 45 hours. Each delay gets ±20% jitter, so a burst of failures does not come back at your endpoint in lockstep — treat the column as approximate.

After the last one the delivery is marked dead and never retried automatically. You can still replay it by hand.

What counts as a failure

Anything that is not 2xx. Explicitly:

Response
2xxSuccess. Resets the endpoint’s failure counter.
410 GoneDead immediately, and the endpoint is disabled. See below.
3xxA failure. We never follow redirects — see below.
4xx (other)A failure. Retried.
5xxA failure. Retried.
Timeout, DNS failure, TLS error, connection refusedA failure. Retried.

A 401 or 404 during your own deploy is far more often transient than permanent, so we retry them rather than giving up. That is a deliberate choice: it means a five-minute deploy window costs you nothing.

410 Gone is the one that stops everything

410 is the single status that means “stop, permanently”. It dead-letters that delivery on the spot — no remaining retries — and flips the endpoint to auto_disabled, so no further events are delivered to it at all until you re-enable it.

Return 410 only when you mean it. Some frameworks return 410 for a soft-deleted route; if that is yours, make sure it cannot fire on your webhook path. The alternative — us hammering a decommissioned URL for months — is why we honour it.

Everything else, including 404, retries.

Redirects are never followed

A 3xx is treated as an ordinary failed delivery. We do not follow the Location header, because doing so would let a redirect jump past the SSRF checks that ran against the original URL.

If your endpoint has moved, PATCH the endpoint’s url. A redirect will not work.

The timeout

We give up on a single attempt after 10 seconds, measured as a total deadline, not just idle time — trickling bytes will not hold the connection open past it.

Acknowledge fast and process afterwards. A handler that does its real work inline and takes 11 seconds looks identical to an outage from our side, and you get a retry (and a duplicate) for work you already completed.

Delivery is at-least-once

You will receive the same event more than once. Not as a rare edge case — as a normal consequence of retrying: your handler can succeed and time out on the response, and the retry that follows is a second copy of work you already did.

Deduplicate on the body’s id:

// The event id is stable across every retry AND across a manual replay.
if (await alreadyProcessed(event.id)) return res.status(200).send();
await recordProcessed(event.id);

DriveCars-Delivery-Id is not the deduplication key — it changes per attempt. That is what makes it useful in logs and useless here.

Order is not guaranteed either. A booking.confirmed that needed three retries can land after a booking.cancelled that succeeded first time. If ordering matters to your logic, compare against the state in data rather than assuming arrival order.

When an endpoint is disabled

An endpoint’s status goes to auto_disabled on any of:

  • 410 Gone — immediately, whatever the failure count.
  • 50 consecutive failures.
  • 72 hours with no successful delivery. Measured from the last success, or from registration if there has never been one.

The second and third are independent — whichever comes first. The 72-hour rule matters because the later backoff steps are long: a low-volume endpoint can be broken for three days without ever accumulating 50 attempts.

A disabled endpoint receives nothing. Events for your partner account are still recorded and readable via GET /v1/events, so nothing is lost — but nothing is pushed either.

We email the contact address on your partner account when this happens.

Re-enabling

curl -sS -X PATCH "https://{your-base-url}/v1/webhooks/42" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"status": "active"}'

Both auto-disable clocks reset in the same write: the consecutive-failure counter goes to zero, and the 72-hour no-success clock restarts from the moment you re-enable. Re-enabling is you telling us the endpoint works again, so neither rule holds its old grievance against it.

Fix the endpoint first, though. A re-enabled endpoint that is still broken simply starts accumulating failures again from zero, and disables itself once more — later than before, but no less certainly.

POST /v1/webhooks/{id}/test is the cheapest way to confirm it really is fixed — provided the endpoint is subscribed to ping, which is a good reason to include it in event_types on every endpoint you register. An endpoint not subscribed to ping receives nothing from that route, and you will have to wait for a real event instead.

Fix the endpoint before re-enabling. Events that dead-lettered while it was down are not redelivered automatically — replay them, or read the gap from GET /v1/events.

Inspecting deliveries

curl -sS "https://{your-base-url}/v1/webhooks/42/deliveries?limit=25" \
  -H "Authorization: Bearer $TOKEN"

Newest first, cursor-paginated like every other list — follow next_cursor.

{
  "id": 5591,
  "event_id": "evt_9c1f4a7b2e8d05364f1a9b7c3d2e5f80",
  "endpoint_id": 42,
  "status": "dead",
  "attempt": 9,
  "next_attempt_at": "2026-09-09T12:00:00.000Z",
  "replay_of": null,
  "attempts": [
    { "at": "2026-09-07T10:00:00.000Z", "outcome": "failed", "statusCode": 502, "bodyExcerpt": "upstream timeout" }
  ],
  "created_at": "2026-09-07T10:00:00.000Z",
  "updated_at": "2026-09-09T09:00:00.000Z"
}

attempts is the per-attempt log — the status code we got and up to 1 KB of your response body. That excerpt is usually the fastest way to find out what your own endpoint said when it failed, especially behind a proxy that swallowed the error.

The log is capped at the 20 most recent attempts. With a nine-attempt ceiling per delivery you will not hit that on a single delivery; a heavily replayed one can.

status is one of pending, in_flight, succeeded or dead.

Replaying

curl -sS -X POST "https://{your-base-url}/v1/deliveries/5591/replay" \
  -H "Authorization: Bearer $TOKEN"

Note the path: /v1/deliveries/{id}/replay, keyed on the delivery id, not the endpoint id.

This queues a new delivery of the same event to the same endpoint, with a fresh attempt count and its own full retry schedule. The response is the new delivery row, with replay_of pointing at the original.

The event id in the body is unchanged — so a replay of something you already processed hits your deduplication and does nothing. That is the intended behaviour, and it is what makes replaying a whole range safe.

The endpoint must be active. Replaying to a disabled endpoint dead-letters without sending. Re-enable first.