Use the event ID to stop repeats

Save each verified provider event in a durable inbox before acknowledging receipt. A duplicate delivery links to that item and does not create another financial effect. The worker marks the item processed in the same transaction as its state and ledger update; receipt alone is not completion.

Check the current payment state

Read the stored state and compare it with the requested move. If an authorized event arrives after a captured event, keep the payment captured and record the late arrival. If an event conflicts with the local record, fetch the provider object or send it to review.

Choose an authority for conflicts

A late event may be valid and still be stale. Keep the provider object ID, event creation time, delivery time, event type, and local state version as separate fields. When a new event ID conflicts with a final local state, query the provider object by its stable ID. Do not use the newest delivery timestamp as the winner. A provider lookup can settle whether the payment is captured, refunded, or still pending. If the lookup itself fails, keep the event in a review queue and leave the ledger unchanged. Record which source resolved the conflict and when.

Keep notifications separate from ledger effects

A second webhook delivery may be useful for support even when it has no financial effect. Store each delivery attempt with its receipt time and verification result, but post the capture ledger entry only once. Send a customer receipt only on the first valid move to captured. If a late authorized event follows capture, it can be acknowledged and linked without another receipt. For a refund event, check the refund operation ID as well as the charge ID. A refund on the same charge is a new money event, not a reason to reopen the original capture.

Split receipt from processing

A webhook receiver has two jobs. First, verify the provider signature on the raw request body and store the event ID and payload. Then return success after durable receipt. A worker can make the slower business decision later. Stripe asks handlers to return a successful response before complex work and requires the raw body for signature checks. If the receiver acknowledges before saving the event, a crash loses it. If it waits for a slow ledger job, the provider may retry while the first job still runs.

Test a synthetic captured event under three faults: the worker is down, the receiver crashes before the inbox insert, and the worker crashes after posting the ledger entry. The first case should leave one stored event ready for later work. The second should return an error so the provider can retry. The third should resume from the stored event without a second credit. Keep separate fields for delivery count, process state, and money effect ID. This lets support see why an event was received twice while the customer got one receipt.

Make worker recovery atomic

The receiver saves a verified event to a durable inbox; the worker applies it later. Store provider account and event ID under a unique key. Keep delivery attempts separate from the processing state. An inbox row marked received is not proof that processing finished. A retry must wake or inspect the unfinished row rather than discard it as already done.

In the worker transaction, lock or claim the inbox item, check the current payment facts, insert the unique financial effect, update local state, and mark processing complete together. Use an economic key as well as the event ID: provider account, effect type, and capture or refund ID. Different event IDs can describe the same capture; distinct refunds on one charge still need distinct postings.

Run PAY-07 with an older authorization after a processed capture. Keep capture and record the late history. Then crash the worker after its database commit but before it acknowledges the queue. Recovery must see the completed inbox item and existing effect. For a capture received first, query the provider object rather than pretending the missing authorization means no capture happened. If lookup is unavailable, retain the item pending with a retry owner. Stripe explains duplicate and unordered delivery.

Evidence to retain

Save the raw provider event IDs, receipt order, stored payment state, and ledger entry count. Show why the late event was ignored or queued.

Sources

Put this into practice

Replay a captured event before an authorized event in staging. Add this case to the webhook test suite before changing the handler. See our webhook security and payment gateway testing service. To check a live flow, request a security review.