2. Implement Idempotency
The same payment might arrive multiple times due to network issues:
def charge_card(order_id, amount, token):
# Use order_id to prevent double charging
idempotency_key = f"charge_order_{order_id}"
return stripe.Charge.create(
amount=amount,
currency='usd',
source=token,
idempotency_key=idempotency_key # Stripe won't double-charge
)
Idempotency is what keeps a retried request from charging the same card twice. Always send an idempotency key. Most payment providers support them natively, but implement the check at your application level too.
The mistake, and the one-line fix. From chapter 7.
1. Always Use Idempotency Keys
Every mutating call to your processor (charge creation, subscription creation, refund, plan change) takes an idempotency key. Use it. The key should be deterministic from your domain identifier (the order ID, the subscription event ID), not a fresh UUID every retry. If you generate a new key on each retry, the API treats every call as new and you double-charge.
# WRONG - each retry creates a new charge
def renew(subscription):
return stripe.Charge.create(
amount=subscription.amount,
customer=subscription.customer_id,
idempotency_key=str(uuid.uuid4()), # NEW KEY EVERY CALL
)
# RIGHT - retries are deduplicated
def renew(subscription, cycle_number):
return stripe.Charge.create(
amount=subscription.amount,
customer=subscription.customer_id,
idempotency_key=f"sub_{subscription.id}_renew_{cycle_number}",
)
The processor will return the original response on subsequent calls with the same key, even across retries spanning hours. This is the single most important pattern for not double-charging customers when your network blips, your worker dies mid-call, or your retry library is misconfigured. Idempotency-key TTLs are usually 24 hours (Stripe) to indefinite (Adyen) - check your provider.
Implementation: src/examples/7-subscriptions-recurring-billing/subscription_testing.py
The provider key covers the request you send. This covers the webhook that comes back. From chapter 5.
4. Handle Idempotency
Idempotency matters because webhooks get delivered more than once, and your handler has to be fine with that:
def fulfill_order(order_id):
with transaction.atomic():
order = Order.objects.select_for_update().get(id=order_id)
if order.status == 'fulfilled':
return # Already processed this webhook
# Fulfill the order
order.status = 'fulfilled'
order.fulfilled_at = datetime.now()
order.save()
# Send confirmation email, etc.
send_order_confirmation(order)
Idempotency is non-negotiable in payment systems. Payment providers will retry webhooks, network issues can cause duplicate deliveries, and race conditions can trigger multiple handlers. Every webhook handler and payment operation must be safe to execute multiple times with the same result.