Technical Guide

Customers Get Free Orders: ChatGPT Stripe Webhook Chaos

Duplicate webhooks skipped charges; we added idempotency and audit checks to recover every dollar.

January 15, 2025 5 min read

The problem

Our subscription box client discovered customers were receiving $89/month premium boxes without being charged. The accounting team found $127,000 in unprocessed payments over 3 months. Some savvy customers had figured out they could spam-click the checkout button and receive multiple shipments while only being charged once. The fulfillment center was shipping orders marked as "paid" but Stripe showed no corresponding charges.

How AI created this issue

The developer asked ChatGPT to "implement Stripe webhook handling for order fulfillment". ChatGPT generated this problematic code:


// ChatGPT's webhook handler
app.post('/webhook/stripe', async (req, res) => {
  const event = req.body;
  
  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object;
    
    // Find order by payment intent
    const order = await Order.findOne({ 
      stripePaymentId: paymentIntent.id 
    });
    
    if (order && order.status === 'pending') {
      // Mark order as paid and trigger fulfillment
      order.status = 'paid';
      order.paidAt = new Date();
      await order.save();
      
      // Send to fulfillment
      await sendToWarehouse(order);
      
      res.json({ received: true });
    }
  }
  
  res.status(200).send();
});

The critical flaw? No idempotency handling. Stripe can send the same webhook multiple times (for reliability), and network issues can cause duplicate deliveries. Each duplicate webhook marked the order as "paid" again and triggered another shipment. ChatGPT didn't mention webhook signature verification, replay attacks, or the need for idempotency keys.

The solution

  1. Webhook signature verification: Implemented Stripe's signature verification to prevent forged webhooks:
    
    // Secure webhook implementation
    const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
    
    app.post('/webhook/stripe', 
      express.raw({ type: 'application/json' }), 
      async (req, res) => {
        const sig = req.headers['stripe-signature'];
        let event;
        
        try {
          // Verify webhook signature
          event = stripe.webhooks.constructEvent(
            req.body, 
            sig, 
            process.env.STRIPE_WEBHOOK_SECRET
          );
        } catch (err) {
          console.error('Webhook signature verification failed:', err);
          return res.status(400).send(`Webhook Error: ${err.message}`);
        }
        
        // Idempotency key from Stripe event ID
        const idempotencyKey = event.id;
        
        // Check if we've already processed this event
        const processed = await WebhookEvent.findOne({ 
          stripeEventId: idempotencyKey 
        });
        
        if (processed) {
          console.log('Duplicate webhook received:', idempotencyKey);
          return res.json({ received: true, duplicate: true });
        }
        
        // Process the event
        try {
          await processStripeEvent(event);
          
          // Record that we processed this event
          await WebhookEvent.create({
            stripeEventId: idempotencyKey,
            type: event.type,
            processedAt: new Date()
          });
          
          res.json({ received: true });
        } catch (error) {
          console.error('Error processing webhook:', error);
          res.status(500).send('Webhook processing failed');
        }
    });
  2. Payment reconciliation system: Built automated reconciliation to catch mismatches between orders and payments
  3. Order fulfillment locks: Implemented database-level constraints to prevent duplicate shipments
  4. Webhook retry handling: Added exponential backoff and dead letter queue for failed webhooks
  5. Real-time alerting: Set up PagerDuty alerts for payment/order mismatches exceeding $500

The results

  • Recovered $127,000 in missed payments through retroactive billing
  • Zero duplicate shipments after implementing idempotency
  • 99.98% payment accuracy (up from 87%)
  • Reduced chargebacks by 94% as customers couldn't exploit the system
  • Saved $23,000/month in lost inventory and shipping costs
  • Webhook processing time improved 3x with proper async handling

The client learned that payment processing requires battle-tested patterns, not just basic webhook handlers. They now maintain a comprehensive test suite that simulates Stripe's webhook retry behavior, network failures, and race conditions.

Ready to fix your codebase?

Let us analyze your application and resolve these issues before they impact your users.

Get Diagnostic Assessment →