Skip to content

Ordering and deduplication

Events are delivered at least once: the same event may come twice. Act on each webhook-id once — store the ids you processed, in the same transaction as what the event changed.

Events of Order
One payment In order: version N is sent only after N−1 was delivered or failed.
One payout In order, the same way.
One refund Not guaranteed: a refund’s processing may arrive after its succeeded.
Different objects No order between them.

Even with ordered delivery, a retry you resend by hand, or two of your servers racing, can apply events out of order. One rule covers every case: store the version, and ignore an event whose version is not higher — payment_version, payout_version or refund_version.

// Inside one transaction: lock the row, compare, apply.
if (event.payment_version <= order.payment_version) return; // older or the same: nothing to do
order.status = event.status;
order.payment_version = event.payment_version;

Final statuses — succeeded, failed, cancelled — never change, so an older event can never undo them.

A webhook is a signal; the API is the truth. When an event looks missing or out of place, read the object — GET /v1/payments/{id}, GET /v1/payments/{id}/refunds, GET /v1/payouts/{id} — and reconcile your record with it. Run that reconciliation also for orders that stayed open longer than you expect.