One payment was credited twice

In this controlled demo, the same payment notification arrives twice and the app adds the credits twice. Payment providers resend these notifications, and the handler never checks whether it already counted this one.

Technical name: Webhook not idempotentControlled demo · fake data

Why did one payment get credited twice?

Because payment providers can deliver the same webhook more than once, and the handler adds credits every time it runs. In our controlled demo (fake payments), the same event delivered twice gave 20 credits for a 10-credit payment. The fix: record each event's unique ID and add the credits in the same database transaction, so a repeat changes nothing.

How to tell if your app has this

No code needed.

  1. Ask your AI tool: "In my payment webhook handler, what happens if the same event arrives twice? Show me where we check the event ID."
  2. In your payment provider's test mode, resend an event your app already received (Stripe's dashboard has a Resend button on each event). The balance or order should not change the second time.
  3. Check for a table that stores processed event IDs with a unique rule. If there isn't one, a repeat is counted again.

Before and after

Before
Demo app · credits
20 credits
You paid for 10
Payment · demo_001+10
Payment · demo_001 AGAIN+10

Same payment delivered twice. Credited twice.

After
Demo app · credits
10 credits
You paid for 10
Payment · demo_001+10
Payment · demo_001 ALREADY COUNTED+0

The repeat is skipped. A new payment still counts.

Why AI-built apps end up with this

In normal testing every event arrives once, so a handler that simply adds the credits looks correct. Repeats only happen under real conditions, like a slow response or a timeout, which rarely show up while you build.

Fix it yourself

How do I fix it? Paste this into your AI tool

My payment webhook adds credits every time it receives an event, so a repeated delivery credits the account twice. Change the handler so it first verifies the webhook signature, then records the event ID in a table where the ID is unique and adds the credits in the same database transaction. If the event ID was already recorded, return success without changing anything. Also make sure the app never treats a message or redirect in the browser as proof of payment.
Works with Lovable, Bolt, Cursor, Replit and similar tools. Then run the check-up below on your own app.

How we checked the fix

Same request before and after the fix
Same requestBeforeAfter
Same payment event, delivered twiceCredited twice (20)Credited once (10)
A new payment event after thatCreditedCredited

Results from our demo test, run against the same demo before and after the fix.

Common mistakes when fixing this

  • Duplicate deliveries are normal, not a bug in your payment provider. Plan for them.
  • A success page or message in the browser is not proof of payment. Only the verified webhook is.

Questions people ask

Isn't a duplicate webhook the provider's bug?

No. Stripe's documentation says webhook endpoints might occasionally receive the same event more than once, and recommends logging the event IDs you've processed and skipping ones you've already handled.

Why does the check need to be in the same transaction?

If two copies arrive at the same moment, both can pass a separate "have I seen this?" check. Saving the event ID under a unique rule and adding the credits in one transaction means only one copy can win.

Can I just trust the success page after checkout?

No. A redirect or message in the browser can be faked, or shown without a completed payment. Give credits or access only after the verified webhook, or after asking the provider's API.

For developers: the cause and the fix in code
− balance += event.amount
+ if save_once(event.id):  # unique, same transaction
+     balance += event.amount

Simplified. Your stack will look different; the principle is the same.

Learn more