API · Events · Webhooks
Retries & replay
Exponential-backoff retries, delivery attempt visibility, and the 30-day replay window.
Last updated
TL;DR. Live mode retries on exponential backoff for 3 days; test mode retries 3
times over a few hours. After the final attempt the event is marked delivery_failed
and stays replayable for 30 days. Receivers must dedupe on event.id.
A delivery is considered successful when your endpoint responds with a 2xx status code within 30 seconds. Anything else — non-2xx, timeout, TLS error, DNS failure — schedules a retry.
Retry schedule
| Mode | Attempts | Window | Cadence |
|---|---|---|---|
live | 9 | 3 days | 1m, 5m, 30m, 2h, 6h, 12h, 1d, 2d, 3d |
test | 3 | ~6 hours | 1m, 30m, 6h |
Cadences are approximate; jitter is added to spread load. If you're staring at the wall clock waiting for the 5-minute attempt, expect anywhere from 4–7m.
A 410 Gone response from your endpoint stops retries immediately and disables the endpoint. Use it when you've decommissioned a handler permanently.
Inspecting deliveries
Every event records its delivery history. Fetch with:
GET /v1/events/evt_1Pq3Kc...The response includes:
{
"id": "evt_1Pq3Kc...",
"object": "event",
"type": "filing.accepted",
"delivery_attempts": 3,
"next_attempt_at": 1745254800,
"last_response": {
"status": 503,
"body_excerpt": "upstream timeout",
"received_at": 1745251260
},
"status": "pending"
}status is one of pending, delivered, or delivery_failed. After 9 live-mode
attempts (or 3 test-mode attempts) the status flips to delivery_failed and no further
retries are scheduled.
You can also list all attempts for an endpoint:
GET /v1/webhook_endpoints/whe_.../delivery_attempts?status=failed&limit=100Replay
If your endpoint was down — a deploy went sideways, your queue backed up, your DNS flapped — replay the missed window:
POST /v1/webhook_endpoints/whe_.../replay
{
"since": "evt_1Pq3Kc..."
}since accepts either an event ID (resumes immediately after that event) or an ISO
8601 timestamp (resumes from the first event at-or-after that instant). Pass
event_types: ["filing.*"] to scope the replay to a subset of your subscription.
Replay is queued — not synchronous. The replay job emits a
webhook_endpoint.replay_started event followed by re-deliveries that carry a
replayed: true flag in the envelope. The 5-minute replay-attack window is recomputed
from the replay timestamp, not the original created.
Replay is only available for the last 30 days. After that, events are purged and cannot be re-delivered. If you need long-term reconstruction, mirror events into your own store on receipt.
What happens after final retry
attempt_9 fails
→ event.status = delivery_failed
→ endpoint.consecutive_failures += 1
→ if consecutive_failures >= 50, endpoint auto-disabled
→ email alert to the endpoint owner
→ event remains replayable for 30 daysAn auto-disabled endpoint stops receiving new deliveries until you re-enable it with
POST /v1/webhook_endpoints/{id}/enable. Re-enabling does not auto-replay the missed
events — call replay separately with the appropriate since.
Idempotency
Retries and replays mean the same event.id may arrive at your handler more than once.
Your handler must be idempotent. The cleanest pattern:
INSERT INTO processed_events (event_id, processed_at)
VALUES ($1, now())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;If the RETURNING row is empty, you've already processed this event — return 200 and
short-circuit. This is cheap, race-safe, and handles every redelivery path (retry,
replay, manual re-trigger).
event.id is monotonic per event but not per data.object — a single resource may
emit many events with the same data.object.id and different event.ids. Dedupe on
event.id, route on data.object.id.
Related
- Signing — verify before deduping.
- Ordering — replays preserve per-entity sequence.
POST /webhook_endpoints/{id}/replay