Skip to content

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.succeeded can arrive before payment_intent.claimed. Decide by the object's status, 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

  1. 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.
  2. 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.
  3. 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).
  4. app.status_changed, consent.revoked, settlement.paid and 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 fresh t, 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 204 is 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), type and endpoint_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, even delivered) back with a fresh retry cycle (attempts from 0) and sends it at once: with endpoint_id to that endpoint (of the same environment — otherwise 409 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 is 409 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/test to endpoint №1 (or the first endpoint), POST …/webhook-endpoints/{eid}/test to the endpoint you pick (whatever its filter or status). If it fails it is retried like any other. 20 a minute.