Retries & ordering
What happens when your endpoint is down, and why an event arrives twice.
When your endpoint fails, Wajub delivers the event again, so your handler can run twice on the same event. This page covers what Wajub guarantees and the handler shape that survives a duplicate.
What Wajub guarantees
At least once, not exactly once. Every event reaches your endpoint at least one time. Some will reach it more than once. Building for exactly-once delivery is building for something no webhook system provides.
One rare case bends that promise: deliveries stop while Wajub restricts your webhooks, as part
of a restriction on your account or its closure. Events are still recorded and listed by
GET /events, but none is sent, to your endpoints or to wajub listen. The events recorded during
the restriction are not sent when it lifts: read them with GET /events and replay the ones you
need with POST /events/{id}/resend.
This is why a daily reconciliation is a required part of an integration, not a safety net you may skip. See The return page is not a confirmation.
No ordering guarantee. Events are delivered as they are produced, with retries
interleaved. A payment.succeeded can land before the payment.processing that preceded
it. Never infer a payment's state from the order events arrive in. Read the state from the event
body, or fetch the payment.
When a delivery counts as failed
A delivery succeeds when your endpoint answers with any 2xx inside 10 seconds. Anything
else is a failure.
| Your response | What happens |
|---|---|
2xx | Delivered. Nothing more. |
5xx, or a network error | Retried |
Any 4xx | Retried |
410 Gone | Not retried. Your endpoint is saying it will never accept this event |
| No response within 10 seconds | Retried |
A 3xx redirect | Retried. Redirects are never followed, so a redirect never delivers |
Only a 410 ends the delivery
A 400 or a 404 is retried like a 500, so an endpoint that answers 404 during a deploy
still gets the event once it is back. Return 410 only when you want Wajub to stop trying this
delivery for good, and 2xx once you have it.
How long Wajub keeps trying
Wajub keeps trying for 72 hours after the first attempt. Once the 72 hours are up, the delivery
is marked failed and Wajub stops. Your endpoint is not disabled, so the next event will still
be attempted, and you can replay the failed one by hand from Konsole or with
POST /events/{id}/resend. If you delete or disable the endpoint while a delivery
is still being retried, the attempts left are abandoned.
Deduplicating on the event id
id in the body, an evt_… value, identifies the event. It is the same on every retry, a
manual resend included, and the same for every endpoint that receives this event. Store it, and
skip an event whose id you have already handled: shipping an order, crediting an account or
sending a receipt then happens once.
The handler shape
Four steps, in this order. The order is the point.
# The same event, arriving a second time after a timeout.
# Its body id is the one of the first attempt.
{"id":"evt_aio5DpN577tNU2vOxdmuZGhT","event":"payment.succeeded","data":{…}}200 means received, not processed
Acknowledge on receipt and process asynchronously. If your own processing can fail, retry it from your own job queue. Wajub's redeliveries are not a way to replay your business logic: they stop after 72 hours.
Mark the event as handled before doing the work, not after. If you mark it after, two retries arriving close together can both pass the check and both run.
Inspecting and replaying
Every attempt is recorded with its status code, response body and latency in Konsole. Once your endpoint is fixed you can replay a single delivery, or up to 100 failed deliveries at once, with the original payload and a freshly signed header.
POST /events/{id}/resend sends the event to every endpoint that has not accepted it yet. Replays
are not counted against your plan, but the same event goes again to the same endpoint at most once
every 5 minutes, from Konsole or from the API. An endpoint inside that delay is skipped, and when
every endpoint is, the call answers 429 with error_code: event_resend_cooldown and a
Retry-After header. That 429 is not the rate limit: resending before
Retry-After gets the same answer.
Webhook deliveries
| Method | Endpoint | Status | Duration | Attempt |
|---|---|---|---|---|
| POST | /webhooks/wajub | 500 | 10021 ms | 1 |
| POST | /webhooks/wajub | 500 | 240 ms | 2 |
| POST | /webhooks/wajub | 200 | 142 ms | 3 |