Skip to content

Retries & ordering

What happens when your endpoint is down, and why an event arrives twice.

Your endpoint will go down at some point, and Wajub will retry. That is the easy half. The harder half is that a retry means your handler runs a second time on an event it may already have processed, and if it ships an order every time it runs, it ships that order twice.

This page covers what Wajub guarantees, what it does not, and the handler shape that survives both.

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.

No ordering guarantee. Events are delivered as they are produced, across a queue, 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 responseWhat happens
2xxDelivered. Nothing more.
5xx, or a network errorRetried, following the schedule below
408 or 429Retried, following the schedule below
Any other 4xxNot retried. Treated as a permanent rejection
No response within 10 secondsRetried
A 3xx redirectRetried. Redirects are never followed, so a redirect never delivers

The retry schedule

Five attempts in total, then the delivery is marked failed.

AttemptSent
1immediately
230 seconds after attempt 1 fails
31 minute later
45 minutes later
510 minutes later

The whole sequence spans about sixteen minutes. After the fifth failure 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.

Deduplicating: two keys, two meanings

Every delivery carries both. They are not interchangeable, and picking the wrong one produces a bug you will not see until you add a second endpoint.

id in the body, an evt_… value, identifies the event. It is the same on every retry, and the same for every endpoint that receives this event.

X-Wajub-Delivery-Id in the headers, a whd_… value, identifies the delivery to one endpoint. It is stable across the five retry attempts, and different for each endpoint.

Which one to store

Deduplicate on the body id when the work must happen once per business event, which is almost always the case: shipping an order, crediting an account, sending a receipt. Use X-Wajub-Delivery-Id when you are tracking delivery health per endpoint rather than business effects.

The handler shape

Four steps, in this order. The order is the point.

# The same event, arriving a second time after a timeout.
# Body id is identical, delivery id is not.
X-Wajub-Delivery-Id: whd_7Yh2MpL4tRb3nP8sZcXv
{"id":"evt_aio5DpN577tNU2vOxdmuZGhT","event":"payment.succeeded","data":{…}}

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.

Reconciling with the request that caused the event

The body carries the request that produced the event:

JSON
{
"id": "evt_aio5DpN577tNU2vOxdmuZGhT",
"event": "payment.succeeded",
"data": {
"id": "trx_CSUGajfv9xh0XQ5wu2lx",
"status": "succeeded"
},
"livemode": true,
"pending_webhooks": 0,
"api_version": "2026-09-01",
"request": {
"id": null,
"idempotency_key": "idem_kZ3qP8mWvL2xR7tB5nY4hC6dF9jS1aG0"
},
"created": "2026-01-15T10:30:00Z"
}

request describes the event record, not your API call. request.id is null, and request.idempotency_key is an idem_… value generated when the event was stored. Neither one carries back the Idempotency-Key header you sent.

Match an event to an order through the resource instead: data.reference is the reference you supplied when you created the payment, and data.metadata is yours to use. Both survive every retry, because they are part of the resource rather than the delivery.

`livemode`, not `sandbox`

The webhook envelope uses livemode, which is the inverse of the sandbox field returned by the REST API. A sandbox event carries "livemode": false.

api_version is the platform's current version at the moment the event was recorded, and the body is shaped for it. Pinning your account or a request to an older version changes what your API calls return, never what your endpoint receives.

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.

Webhook deliveries

last hour
MethodEndpointStatusDurationAttempt
POST/webhooks/wajub50010021 ms1 of 5
POST/webhooks/wajub500240 ms2 of 5
POST/webhooks/wajub200142 ms3 of 5

What did you think of this content?