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 doorder.status = event.status;order.payment_version = event.payment_version;Final statuses — succeeded, failed, cancelled — never change, so an older event can never undo them.
Missed an event?
Section titled “Missed an event?”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.