Delivery & idempotency
At-least-once delivery and how to dedupe.
ShiftxPay delivers webhooks at-least-once. A single event can arrive more
than once — for example when your server processed a delivery but its 2xx
response was lost, so the dispatcher retried. Your handler must therefore be
idempotent: processing the same event twice must have the same effect as
processing it once.
Retries and backoff
A delivery is queued the moment an event fires and picked up by a dispatcher that
runs on a short interval. Any response outside the 2xx range — or a connection
error, timeout, or TLS failure — counts as a failed attempt and is rescheduled.
Backoff is exponential: roughly 1, 2, 4, 8, … minutes between attempts,
capped at 1 hour. Attempts continue until the delivery succeeds or is
exhausted, at which point it is marked failed with a last_error.
You can inspect and replay attempts from the deliveries log:
GET /v1/merchants/me/webhooks/{id}/deliveries
POST /v1/merchants/me/webhooks/{id}/deliveries/{delivery_id}/retryEach delivery record exposes event_type, attempts, delivered, failed,
status_code, last_error, and next_attempt_at (present only while the
delivery is still pending).
How to deduplicate
A retried delivery re-POSTs a byte-identical body (only the signing
timestamp in the ShiftxPay-Signature header changes per attempt). Payment
payloads currently carry type, payment_id, and status — there is no
separate event-id field on the wire. Dedupe on what is delivered:
- Key on the
(type, payment_id, status)tuple, or a hash of the raw request body, and record processed keys (a unique index or a short-lived cache). - On a repeat, return
2xximmediately and skip the side effect.
Because the same logical event (e.g. payment.succeeded) can also arrive from
two sources — your synchronous confirm and the PSP's signed webhook — the tuple
above is the safest natural key: both deliveries describe the same payment in the
same state.
The webhook is your backstop
The synchronous confirm flow tells you a payment's outcome inline, but a buyer can close the tab, a network can drop, or your process can restart mid-request. The webhook is the durable, retried channel that guarantees you eventually learn the final state even when the synchronous path is interrupted.
Treat the webhook — not the browser round-trip — as the source of truth for
fulfilment. Reconcile optimistic UI from the confirm response, but only release
goods, settle orders, or send receipts once the corresponding
payment.succeeded (or payment.refunded) webhook has been verified and
recorded.