October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsSlow PC?RecommendedPC slow today? Run a repair scan before it gets worseResolve common Windows issues and optimize system performance.Scan NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk6 min

One Payment Event, Two Credit Grants: How a TypeScript Webhook Handler Can Double-Credit a Customer

A webhook handler that assumes one delivery per payment can grant credits twice. Here is how Stripe retries and unordered events cause it, and the layered fix: verify the raw body, dedupe by event ID, and enforce a unique ledger key.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • 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.

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
Sale
HTML and CSS: Design and Build Websites
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  1. Read the raw body. Verify the signature against the untouched request bytes, the Stripe-Signature header, and your endpoint’s signing secret, using stripe.webhooks.constructEvent from 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.
  2. Record the event ID durably. Insert event.id into 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.
  3. 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.
  4. 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.
  5. 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.

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
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • 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
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

“

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
PC Slower Than It Used to Be?Free scan - under a minute

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.