Payment Webhooks and Reconciliation
Payment webhooks and reconciliation keep three accounts of reality aligned: what the provider reports, what your application records, and what the relevant financial reports establish. Build for duplicate and delayed delivery, interrupted processing, and outcomes that remain unknown. A successful webhook response is not a statement that all related business work has finished.
Start with a durable receipt, an explicit processing state, and a provider-specific recovery contract. Keep payment, refund, dispute, settlement, and fulfillment states distinct enough that one delayed message cannot silently rewrite another outcome. The example below uses fictional data and performs no payment action.
TL;DR
- Verify incoming requests using the provider's supported signature procedure.
- Separate receipt acknowledgment from completed processing.
- Deduplicate deliveries and business effects at their appropriate scopes.
- Track each refund's actual state and identity.
- Reconcile defined currencies, periods, and transaction populations independently.
Quick Example
This SQLite example starts after the trust boundary: its inputs are fictional, already-trusted capture records. The two signed lines illustrate balanced arithmetic only. They are not a financial chart of accounts or a production payment ledger.
Test it by injecting a failure after the first line: the inbox, posting, and lines should all roll back, and a retry should then succeed. A second event for the same capture should not post again, and conflicting amounts should roll back. An in-memory database can't test durable HTTP receipt, concurrent workers, provider signatures, or real refunds.
Core Concepts
Stripe documents duplicate deliveries and does not guarantee event delivery order. It recommends official-library signature verification using the unchanged raw request body. Its guidance also distinguishes event IDs from cases where separate event objects describe duplicate effects. Treat those as provider-specific rules, not a universal webhook schema. Stripe webhook guidance.
Separate event identity, provider object identity, and business-operation identity. One event can be delivered repeatedly; different events can refer to one operation; one payment can have several legitimate refunds. A deduplication key that collapses every update for an object can discard necessary state changes.
The toy capture example intentionally models one completed capture per payment ID. Real integrations may need capture IDs, tenant/account scope, event type, or another provider-defined identity. Choose the key from the actual operation contract rather than copying the example's table key unchanged.
Design the receipt-to-effect boundary
This editorial matrix describes what to investigate. It is not a tested accounting control or authorization to move money.
Persist accepted work before relying on a later worker. If acknowledgment is sent and the only queued item existed in process memory, a restart can lose work after the provider considers delivery successful. Define the durable boundary and test the corresponding crash point.
When a local transaction updates both receipt and local effect, keep them atomic where appropriate. External fulfillment or messaging introduces another boundary; a database transaction cannot roll back an email or remote shipment request. A durable outbox and idempotent downstream consumer may be needed, with actual integration tests.
Keep refund and ordering semantics explicit
Stripe supports partial refunds and documents pending, failed, and canceled outcomes under its payment-method-specific rules. Creating a refund request does not establish that the customer received funds. Avoid a single boolean that loses the difference between requested, pending, successful, and failed refund work. Stripe refund guidance.
Track individual refund identities and amounts, their parent payment, currency, and the relevant authoritative observations. A second partial refund is not a duplicate of the first merely because both reference the same payment. Likewise, a later failure or reversal must not be discarded because the object was seen previously.
Do not impose one global “statuses only move forward” ordering. Different objects and legitimate reversals have different transition rules. Use documented provider semantics and retrieve current objects when necessary. Event timestamps alone are not an authoritative sequence number for every state transition.
Keep unresolved outcomes visible. If a provider lookup fails, preserve pending reconciliation with a next check and an owner. Guessing success can cause premature fulfillment; guessing failure can cause a duplicate operation. Both are business decisions disguised as error handling.
Reconcile populations before comparing totals
Stripe's balance summary report describes balance activity over a selected period and settlement currency. It explicitly differs from payout reconciliation, which associates transactions with automatic payouts. Select the report that answers the intended question. Stripe balance reporting.
Compare like with like: account scope, currency, period boundaries, gross/net treatment, fees, refunds, disputes, and payout timing. A difference caused by unmatched populations is not evidence of a missing payment. Preserve transaction-level exceptions rather than silently adjusting a total until it matches.
Separate application reconciliation from financial close. Engineering can detect missing records and explain processing gaps; qualified finance owners should approve accounting classifications, adjustments, and settlement interpretations. Balanced toy entries demonstrate an invariant, not correct accounting treatment.
Best Practices
Test crashes before receipt persistence, after persistence, during local posting, and after an external effect. Include duplicate, out-of-order, conflicting, and missing observations. Name exactly which failure the test reproduces and which boundaries remain simulated.
Record safe identifiers and processing outcomes without retaining payment secrets or unnecessary personal data in ordinary logs. Protect payload storage, define access and retention, and preserve enough version context to interpret older events.
Monitor pending age, retry exhaustion, and unresolved reconciliation exceptions alongside HTTP success rates. A green endpoint can coexist with an accumulating backlog of unprocessed work.
Common Mistakes
Bad: Mark an event processed before its effect is durably committed.
Correct: Design and test the atomic boundary so an interrupted worker can recover without skipping or duplicating the effect.
Bad: Deduplicate all refund updates by payment ID.
Correct: Preserve individual refund identities and documented transitions, including legitimate later failures or reversals.
Bad: Compare local payment totals directly with bank deposits.
Correct: Reconcile the appropriate report population, timing, currencies, fees, and payout relationships with finance ownership.
FAQ
Does a 200 response mean the payment is settled?
No. It acknowledges webhook delivery under the endpoint contract. Provider payment state, internal processing, and settlement remain separate observations.
Is the example a signature verifier?
No. It begins with fictional trusted records and demonstrates local transaction behavior only. Use the provider's maintained library and exact raw-body procedure for real requests.
What would change the reconciliation design?
Multiple provider accounts, currencies, partial captures, refunds, disputes, or payout arrangements can change identity and matching rules. Review those requirements before treating one payment row as the full money lifecycle.
Related Topics
- Payment Processing — Understand the broader provider integration.
- Third-Party Integration Resilience — Recover uncertain remote outcomes.
- MongoDB Transactions — Compare local transactional boundaries with external effects.
- Floating-Point and Decimal Arithmetic — Define numeric representation and rounding deliberately.