DEV Community

SaaSFactory
SaaSFactory

Posted on

3 idempotency bugs that double-charge customers in Stripe integrations (with fixes)

Double charges are one of the most expensive bugs a payment integration can ship: they cost you refund labor, chargebacks, and trust. Here are three idempotency mistakes I keep finding in real Stripe integrations, with the fix for each.

1. No Idempotency-Key on retried requests

If your client (or your own retry logic) resends a charges.create or payment_intents.create call after a timeout, without an idempotency key Stripe treats it as a brand new charge.

import stripe

# BAD: a network retry here creates a second charge
stripe.PaymentIntent.create(amount=2000, currency="usd", customer=cust_id)

# GOOD: same key = same result, safe to retry
stripe.PaymentIntent.create(
    amount=2000, currency="usd", customer=cust_id,
    idempotency_key=f"order-{order_id}-attempt",
)
Enter fullscreen mode Exit fullscreen mode

The key must be stable across retries of the same logical operation, not regenerated per HTTP call.

2. Webhook handler doesn't dedupe by event.id

Stripe explicitly says webhooks can be delivered more than once. If your checkout.session.completed handler grants access or ships an order without checking whether that event.id was already processed, a duplicate delivery (common during Stripe retries after a slow 200) double-fulfills the order.

if Event.objects.filter(stripe_event_id=event["id"]).exists():
    return HttpResponse(status=200)  # already handled, ack and exit
Event.objects.create(stripe_event_id=event["id"])
# ... fulfill order
Enter fullscreen mode Exit fullscreen mode

3. Idempotency key reused across genuinely different requests

The inverse bug: reusing the same key for two different orders (e.g. keying only on customer_id instead of order_id) makes Stripe silently return the first charge's result for the second order, so the second customer's payment appears to succeed but no charge was made for their amount.

Key your idempotency key on something unique to the operation (order id, cart id, invoice id) — never on something that repeats, like customer id or a fixed string.


These three account for most of the double-charge and silent-non-charge tickets I've seen in production Stripe integrations. If you want someone to check your actual repo for these and four other common failure modes (webhook signature verification, timestamp tolerance, retry backoff, race conditions on subscription updates), I run a fixed-price, done-for-you review: $39, 48h turnaround, full refund if your repo isn't public — https://buy.stripe.com/6oU4gy6gZ6hYfGH7tu7IY0i

Top comments (0)