Recommended Free Tools
A payment webhook grants credits twice when the handler assumes every delivery is unique and then runs a non-idempotent credit write each time it receives one. The fix is not a single check. The request must be authenticated, the event ID must be recorded durably, and the credit itself must be protected by a uniqueness rule in the database. This walkthrough uses Stripe as the worked example, because its webhook documentation states the retry and ordering behavior that makes the bug likely. It does not describe a specific production incident, and the same pattern applies to any provider that redelivers events.
Why one payment produces two grants
A webhook endpoint is an HTTP handler that a payment provider calls when something happens in its system, such as a successful charge. The handler is supposed to run once per event. The provider does not promise that. Stripe’s current Webhooks documentation (accessed October 7, 2026) says that webhook endpoints “might occasionally receive the same event more than once.” A handler that adds 500 credits on every checkout.session.completed call will therefore add them again whenever a delivery is repeated.
As an Amazon Associate I earn from qualifying purchases.
Two behaviors make this more likely than most developers expect:
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware match- Retries. If your endpoint does not return a 2xx response in time, Stripe retries. For live-mode endpoints, automatic retries run for up to three days with exponential backoff. A slow handler that finishes its database write but times out before responding will look like a failure to Stripe, and the retry will arrive after the credit is already committed.
- No ordering guarantee. Stripe does not guarantee that events arrive in the order they were generated. A later state change can reach your server before an earlier one. Logic that says “only grant credits when the previous event has been seen” can fail in either direction.
The bug, then, is not that the provider sends duplicates by mistake. It is that the grant path was written as if one payment could only ever produce one call.
#1 Best Overall
Four different protections, four different failures
Developers often reach for one safeguard and assume the problem is solved. Each safeguard covers a different failure mode, and none of them covers all of them.
| Safeguard | What it protects | What it does not do alone |
|---|---|---|
Raw-body signature verification with constructEvent |
Rejects requests that do not carry a valid Stripe signature for your endpoint secret | Does not stop a legitimately signed event from being redelivered |
| Unique processed-event ID | Prevents the same Stripe event ID from being accepted twice | May not catch two separate Event objects that describe the same payment |
| Unique business key in the credit ledger, written in a transaction | Prevents a second credit from being committed for the same purchase, including under concurrent workers | Does not authenticate incoming requests |
| Stripe API idempotency key | Lets an eligible retried Stripe API request return its saved result | Does not make a write to your own database atomic or unique |
The Stripe API idempotency key deserves a specific warning. It applies to supported API POST requests that your server makes to Stripe, such as creating a charge. Stripe’s documentation notes that saved keys can be pruned after at least 24 hours, and reusing a key after pruning can create a new request. It is useful for your outgoing calls, but it is not a permanent record of what your application has already granted.
Rank #2
- HTML CSS Design and Build Web Sites
- Comes with secure packaging
- It can be a gift option
A safe handler, in stages
The handler below is a sequence of checks, not a copy-paste implementation. Each stage narrows what can happen next.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →- Read the raw body. Verify the signature against the untouched request bytes, the
Stripe-Signatureheader, and your endpoint’s signing secret, usingstripe.webhooks.constructEventfrom the official Node.js SDK. If a framework parser has already converted the body to JSON and re-serialized it, the signature check can fail even for valid events. Most frameworks offer a way to keep the raw buffer for this route; the exact method depends on the framework and the SDK version you have installed, so check the types in your project. - Record the event ID durably. Insert
event.idinto a processed-events or inbox table that has a unique constraint on that column. If the insert fails because the row exists, the delivery is a repeat. Acknowledge it with a 2xx response and stop. Do not enqueue a second grant. - Apply the grant under a business-level unique rule. Write the credit ledger row in a database transaction. Tie the row to a stable business key, such as the payment intent ID, the checkout session ID, or your internal order ID, and place a unique index on that key together with the grant type. This second guard handles concurrent workers, replays that arrive under a different event ID, and distinct Event objects that refer to the same underlying payment object. Stripe’s guidance for duplicate events suggests comparing the object ID with the event type; the unique ledger key is the enforcement point for that comparison.
- Acknowledge promptly, then process if needed. Once the event is durably accepted, return a 2xx response. If the credit work involves slow steps such as calling other services, move that work to a queue and let the worker run it. Stripe recommends this pattern for scalability.
- Make the worker retry-safe and auditable. A queue job that is retried after a crash must reach the same unique key and produce no second row. Log the event ID and business key on every attempt, and keep a reconciliation query that compares paid orders with ledger grants so that any gap is visible.
The following sketch shows the order of operations. It is illustrative and framework-neutral. The raw body type, the database calls, and the transaction code are omitted, and the business key depends on how your product models purchases.
Rank #3
const event = stripe.webhooks.constructEvent(rawBody, signature, endpointSecret);
// Step 2: one durable insert under a unique constraint on event.id.
// If the row already exists, acknowledge and stop.
// Step 3: inside one transaction, insert the credit ledger row
// with a unique key such as (paymentIntentId, "purchase_credits").
// A unique-violation here means the grant already exists.
// Step 4: return 2xx; any slow follow-up runs from a retry-safe queue.
What to check when credits are already doubled
If you have already seen duplicate grants, start by reading your own logs rather than the provider dashboard. Look for pairs of deliveries with the same event ID, and for pairs with different event IDs that share a payment object ID. The first pattern points to a missing or failing processed-event check. The second points to a missing business-key constraint on the ledger. Where the two appear together, the handler likely has both gaps.
When you correct the duplicates, reverse the extra grant through the same ledger path that created it, not by editing a balance directly. That keeps the audit trail complete and makes the reconciliation query match what customers actually received.
Rank #4
- Brand: Wiley
- Set of 2 Volumes
- A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers
What this does not establish
The retry window and ordering behavior described here come from Stripe’s current documentation. They describe the provider’s operating behavior, not a measured rate of duplicate grants across applications. The documentation does not describe any particular application’s schema, transaction boundaries, or incident history, so the sequence above should be read as a design pattern to check against your own code, not as a diagnosis of a specific system.
Free tools Windows power users keep installed
One-click scans. No signup required.
Stripe is used here because it is the documented example. The same reasoning applies to other providers that redeliver events. Confirm each provider’s retry and ordering rules in its own documentation before relying on them.
Best Value
Practical takeaway for the handler
Treat delivery as at-least-once. Verify the raw body, record the event ID under a unique constraint, protect the credit with a business-level unique key inside a transaction, and let queue retries land on that same key. Each layer covers a case the others miss.
Quick Recap
“
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




