Delivery and retries¶
When events arrive, how often, in what order, what counts as success, what happens to an endpoint that keeps failing, and how to get back an event you missed. Take five minutes to read this before an endpoint goes to production.
- You need: a working webhook endpoint (Overview) and a handler that verifies signatures (Signatures).
1. In short¶
- Success = a 2xx within 10 seconds. Everything else (3xx, 4xx, 5xx, a timeout, a connection error) is a failure and is retried.
- At least once: one event can reach one endpoint two or more times. Deduplicate on
id. - No ordering guarantee.
payment_intent.succeededcan arrive beforepayment_intent.claimed. Decide by the object'sstatus, not by the order things arrive in (Idempotent handlers). - Each endpoint on its own: an event has its own delivery (attempts, retries, status) to every active endpoint of its environment whose filter takes it. One endpoint being down does not affect another.
- An event is sent at once when it is committed; if that fails it is retried on the schedule for about two days,
then
failed. An endpoint that fails for five days in a row is turned off. Fetch anything you missed from the Events API.
2. When an event is sent¶
- The event is written to the outbox in the same transaction that commits the change (a payment, a claim, a refund …), and in that same transaction a delivery is made for every active endpoint of the environment whose filter takes it — if the change happened the event exists, if it did not there is none. An event with no endpoint at that moment is still kept and is listed by the Events API.
- Inline attempt. After a payment intent's state changes, a claim, a refund, a simulation or a console «send
again», one attempt is made straight away (a vending machine should not wait a minute for
requires_capture). It is best effort: up to 8 at once in a process; under load the delivery is left to the job — a minute late, never lost. - The job sends due deliveries every minute (50 at a time). The inline attempt and the job never send the same delivery at the same time (a lease).
app.status_changed,consent.revoked,settlement.paidand the events of a console «test payment» confirmation (…/test-intents/{pi}/confirm) get no inline attempt — they come with the job, within a minute.
3. Request and response¶
POST; the body is the JSON written once when the event was created — the same bytes on every retry and to every endpoint. Each attempt carries a signature with a fresht, made with that endpoint's secret (Signatures).- Timeout 10 seconds (5 s to connect, 5 s for TLS). Queue your work and answer 2xx straight away.
- Redirects are not followed: a 301/302/307 is recorded as a failure. Set the URL to its final address.
- The response body is not read (the first 64 KiB are discarded) — answer anything; a
204is enough. - The address is checked after DNS: nothing is ever sent to a private, loopback or link-local address ("refused: not a public address").
- An unsigned event is never sent: if the platform cannot open a secret, the delivery waits and is retried like a failure (this does not count against your endpoint).
4. Retry schedule¶
| Attempt | After the previous failure |
|---|---|
| 1 | at once (or the next minute's job) |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 6 hours |
| 7 | 12 hours |
| 8 | 24 hours |
8 attempts in all, about 45 hours (~2 days) from the first to the last. If the 8th fails, that delivery is
failed and is not sent again automatically. Times can slip by up to a minute, the job's period.
5. A failing endpoint — warning and auto-disable¶
From the moment every delivery to an endpoint starts failing (failing_since) the streak is counted; a single
successful delivery (even a late one) resets it. The check runs every 15 minutes:
| For | What happens |
|---|---|
| 24 hours, everything failing | The organisation's admins get one notification, «A webhook is failing» (the app's name and environment — not the URL) |
| 5 days, everything failing | The endpoint becomes auto_disabled, with a disabled_reason; the deliveries it was still owed become skipped; the admins are told «A webhook was turned off» |
To turn it back on: fix the endpoint, fetch what it missed from the Events API, then PATCH
…/webhook-endpoints/{eid} {"status": "active"} — the streak starts afresh. Events from while it was off are not
sent to it automatically; use «send again» in the console if you need to.
6. Delivery states, retention, the console¶
status |
Meaning |
|---|---|
pending |
Waiting its turn (or a retry is due) |
delivered |
Got a 2xx |
failed |
All 8 attempts failed |
skipped |
The endpoint was off when the delivery came due; on the console's list — an event that was owed to no endpoint at that moment. Not sent again automatically |
- The URL and the secret are read when a delivery is made: change the URL and the next retry of an old event goes to the new URL. Delete an endpoint and its deliveries go with it.
- Events are kept for 30 days from when they were created, then deleted (one with a delivery still pending stays until it is done) — the Events API reaches back 30 days too.
The app's «Webhook deliveries» in the developer console (Developer sandbox):
- The list, newest first — filter by
status, environment (test/live),typeandendpoint_id: attempts, the last HTTP code and error, when the next attempt is, the body, and every endpoint's delivery (deliveries[]). One event:GET /dev/apps/{id}/webhook/events/{event_id}. - «Send again» (
POST …/webhook/deliveries/{event_id}/retry[?endpoint_id=]) — puts any event (failed,skipped, evendelivered) back with a fresh retry cycle (attempts from 0) and sends it at once: withendpoint_idto that endpoint (of the same environment — otherwise409 environment_mismatch), without it every delivery of the event again (or, if it had none, to the active endpoints whose filter takes it now). No endpoint at all is409 no_webhook. webhook.test— sends a signed test event down the same path as a real one, right away, and shows the outcome (ok,status_code,error,duration_ms):POST …/webhook/testto endpoint №1 (or the first endpoint),POST …/webhook-endpoints/{eid}/testto the endpoint you pick (whatever its filter or status). If it fails it is retried like any other. 20 a minute.