ShiftxPay
Webhooks

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}/retry

Each 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 2xx immediately 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.

On this page