BNSeven
Developers

Webhooks, Idempotency, and Why Payment Integrations Break

Most payment-integration bugs aren't about the happy path. They're about retries, duplicate deliveries, and out-of-order events.

The BNSeven Team

Editorial · September 5, 2026 · 4 min read

Share

Payment integrations tend to work perfectly in initial testing and then break in production in ways that are hard to reproduce — a customer charged twice, a subscription that never updated after a successful renewal, a refund that got processed but never reflected in the seller's own records. Almost all of these trace back to two related concepts that don't get enough attention until they cause an incident: idempotency and webhook delivery guarantees.

Webhooks are notifications, not commands — and they arrive at least once

A webhook is how a payments platform tells your system "something happened" — a payment succeeded, a subscription renewed, a dispute was opened. The critical detail: most well-designed webhook systems guarantee at-least-once delivery, not exactly-once. That means your system has to be prepared to receive the same event more than once — because of a network blip, a timeout on your server's response, or the platform's own retry logic — and handle it safely both times.

A webhook handler that isn't built for this will, sooner or later, process a "payment succeeded" event twice: sending a duplicate receipt email, double-crediting an internal ledger, or granting a customer double the entitlement they paid for.

Idempotency is the fix, and it has to live at the right layer

An idempotent operation produces the same result no matter how many times it's applied — running it twice has the same effect as running it once. For payment integrations, this usually means: every meaningful state change should be guarded by a unique identifier tied to the underlying event (the payment ID, the specific webhook delivery ID, or both together), checked against what's already been processed, before any side effect happens.

The subtle failure mode here is a "check then act" pattern that isn't actually atomic: checking whether an event was already processed, and only then processing it, has a race condition if two copies of that same event arrive close together and both pass the check before either one records that it's been handled. The fix generally requires a real database constraint (a unique index on the event identifier) doing the enforcement, not just application-level logic that assumes requests arrive one at a time.

Webhooks aren't guaranteed to arrive in the order the underlying events happened. A "subscription renewed" event and a "payment failed" event for a later billing cycle could, in principle, arrive in either order relative to each other if there's any retry or delay involved. A robust integration keys its state transitions off the state of the resource itself (checking the current status before applying a transition) rather than assuming the most recently received webhook necessarily represents the most recent true state.

Signature verification isn't optional

A webhook endpoint is a public URL, by necessity — the payments platform has to reach it from the outside. That means it's also a public URL that anything on the internet can attempt to POST to, including someone attempting to forge a fake "payment succeeded" event. Every legitimate webhook system signs its payloads (typically HMAC-based) so the receiver can verify the request actually came from the platform and wasn't tampered with in transit — and that verification has to happen before any part of the payload is trusted or acted on, not as an afterthought once code is already parsing fields out of it.

Retry your own failures, don't swallow them

If your webhook handler fails partway through — a database error, a downstream service timeout — the right response is usually to return a failure status so the platform's own retry mechanism tries again later, not to catch the error, log it, and return success anyway. Returning a false "success" to make a monitoring dashboard look clean is one of the more common ways a real financial event silently never gets processed, discovered only much later during a reconciliation exercise.

The underlying discipline

None of this is unique to payments — it's the same set of concerns that apply to any distributed system passing messages between services that can't guarantee perfect, ordered, exactly-once delivery. Payments integrations just make the consequences of getting it wrong immediately visible, in dollar amounts, to a customer who notices right away.