Skip to main content

Command Palette

Search for a command to run...

Stripe Webhooks: Handle Payment Events Securely

Published
6 min read
T

Welcome to TopperBlog! 👋

I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.

🎯 What I Write About: • AI/ML Engineering & LLMs • Web3 & Blockchain Development
• System Design & Architecture • Interview Preparation (FAANG) • Freelancing & Remote Work • Modern Tech Stacks (Next.js, React, Rust, TypeScript) • Performance Optimization & Best Practices

💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.

📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.

🌐 Let's connect and grow together in this amazing tech journey!

#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering

Stripe Webhooks: Handle Payment Events Securely

The Payment Integration That Made Us $1M (And Almost Broke Everything)

Accepting payments should be simple. Our first attempt was a disaster. Here's what we learned.

Table of Contents

  • E-commerce in 2026
  • Integration Architecture
  • 5 Implementation Patterns
  • Security Requirements
  • Webhook Handling
  • Error Recovery
  • Compliance
  • FAQ
  • Production Checklist

E-commerce Landscape 2026

Online payments are table stakes.

Payment Flow

// Modern payment architecture
interface PaymentFlow {
  checkout: 'Collect payment details';
  process: 'Submit to payment provider';
  confirm: 'Verify payment status';
  fulfill: 'Deliver product/service';
  reconcile: 'Match payments to orders';
}

Revenue Impact

// Payment optimization ROI
const metrics = {
  checkoutConversion: 0.85,    // 85% complete checkout
  paymentSuccess: 0.97,        // 97% payments succeed
  fraudRate: 0.02,             // 2% fraud
  chargebackRate: 0.003        // 0.3% chargebacks
};

// Improving conversion by 5% = huge revenue

Cost of Downtime

Every minute of payment downtime costs money.

Integration Architecture

Building for scale and reliability.

Client-Server Flow

// Secure payment flow
// 1. Client: Create checkout session
const session = await fetch('/api/checkout', {
  method: 'POST',
  body: JSON.stringify({
    items: [{ id: 'product_1', quantity: 2 }],
    successUrl: 'https://example.com/success',
    cancelUrl: 'https://example.com/cancel'
  })
});

// 2. Server: Initialize with payment provider
import { stripe } from './stripe';

app.post('/api/checkout', async (req, res) => {
  const session = await stripe.checkout.sessions.create({
    line_items: req.body.items.map(item => ({
      price: item.priceId,
      quantity: item.quantity
    })),
    mode: 'payment',
    success_url: req.body.successUrl,
    cancel_url: req.body.cancelUrl,
    customer_email: req.user.email
  });

  res.json({ url: session.url });
});

// 3. Client: Redirect to payment page
window.location.href = session.url;

Database Schema

-- Orders table
CREATE TABLE orders (
  id UUID PRIMARY KEY,
  user_id UUID NOT NULL,
  status VARCHAR(50) NOT NULL,
  total_amount INTEGER NOT NULL,
  currency VARCHAR(3) NOT NULL,
  payment_provider VARCHAR(50),
  payment_id VARCHAR(255),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- Order items
CREATE TABLE order_items (
  id UUID PRIMARY KEY,
  order_id UUID REFERENCES orders(id),
  product_id UUID NOT NULL,
  quantity INTEGER NOT NULL,
  price_at_time INTEGER NOT NULL
);

Pattern 1: Payment Intent

Create Intent

// Stripe Payment Intent
import { Stripe } from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

app.post('/api/payment-intent', async (req, res) => {
  const { amount, currency } = req.body;

  const paymentIntent = await stripe.paymentIntents.create({
    amount: amount * 100, // cents
    currency,
    automatic_payment_methods: { enabled: true },
    metadata: {
      orderId: req.body.orderId,
      userId: req.user.id
    }
  });

  res.json({
    clientSecret: paymentIntent.client_secret
  });
});

Confirm Payment

// Client-side confirmation
import { loadStripe } from '@stripe/stripe-js';

const stripe = await loadStripe(PUBLISHABLE_KEY);

const { error, paymentIntent } = await stripe.confirmCardPayment(
  clientSecret,
  {
    payment_method: {
      card: cardElement,
      billing_details: {
        name: 'John Doe',
        email: 'john@example.com'
      }
    }
  }
);

if (error) {
  // Handle error
  console.error(error.message);
} else if (paymentIntent.status === 'succeeded') {
  // Payment successful!
  window.location.href = '/success';
}

Pattern 2: Webhook Handling

Secure Webhooks

// Verify webhook signature
import { buffer } from 'micro';

export const config = {
  api: { bodyParser: false }
};

app.post('/api/webhooks/stripe', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const rawBody = await buffer(req);

  let event;

  try {
    event = stripe.webhooks.constructEvent(
      rawBody,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Handle event
  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePaymentSuccess(event.data.object);
      break;
    case 'payment_intent.payment_failed':
      await handlePaymentFailure(event.data.object);
      break;
  }

  res.json({ received: true });
});

Event Processing

// Process payment success
async function handlePaymentSuccess(paymentIntent) {
  const orderId = paymentIntent.metadata.orderId;

  // Update order status
  await db.orders.update(orderId, {
    status: 'paid',
    payment_id: paymentIntent.id,
    paid_at: new Date()
  });

  // Fulfill order
  await fulfillOrder(orderId);

  // Send confirmation email
  await sendEmail({
    to: paymentIntent.receipt_email,
    template: 'order-confirmation',
    data: { orderId }
  });
}

Pattern 3: Subscription Billing

Create Subscription

// Recurring billing
app.post('/api/subscriptions', async (req, res) => {
  const { priceId } = req.body;

  // Create or retrieve customer
  let customer = await db.customers.findOne({
    userId: req.user.id
  });

  if (!customer) {
    const stripeCustomer = await stripe.customers.create({
      email: req.user.email,
      metadata: { userId: req.user.id }
    });

    customer = await db.customers.create({
      userId: req.user.id,
      stripeCustomerId: stripeCustomer.id
    });
  }

  // Create subscription
  const subscription = await stripe.subscriptions.create({
    customer: customer.stripeCustomerId,
    items: [{ price: priceId }],
    payment_behavior: 'default_incomplete',
    expand: ['latest_invoice.payment_intent']
  });

  res.json({
    subscriptionId: subscription.id,
    clientSecret: subscription.latest_invoice.payment_intent.client_secret
  });
});

Handle Subscription Events

// Webhook for subscriptions
switch (event.type) {
  case 'customer.subscription.created':
    await activateSubscription(event.data.object);
    break;
  case 'customer.subscription.deleted':
    await cancelSubscription(event.data.object);
    break;
  case 'invoice.payment_succeeded':
    await renewSubscription(event.data.object);
    break;
  case 'invoice.payment_failed':
    await handleFailedPayment(event.data.object);
    break;
}

Pattern 4: Error Handling

Retry Logic

// Exponential backoff
async function retryPayment(paymentId: string, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const payment = await processPayment(paymentId);
      return payment;
    } catch (error) {
      if (i === maxRetries - 1) throw error;

      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Failed Payment Recovery

// Handle failed payments
async function handleFailedPayment(invoice) {
  const subscription = await stripe.subscriptions.retrieve(
    invoice.subscription
  );

  // Send email notification
  await sendEmail({
    to: invoice.customer_email,
    template: 'payment-failed',
    data: {
      amount: invoice.amount_due,
      retryDate: new Date(subscription.current_period_end * 1000)
    }
  });

  // Update user status
  await db.users.update(subscription.metadata.userId, {
    subscriptionStatus: 'past_due'
  });
}

Pattern 5: Fraud Prevention

Risk Assessment

// Fraud detection
import { stripe } from './stripe';

const paymentIntent = await stripe.paymentIntents.create({
  amount: 10000,
  currency: 'usd',
  // Enable Radar for fraud detection
  radar_options: {
    session: sessionId
  }
});

// Check risk score
if (paymentIntent.charges.data[0].outcome.risk_level === 'high') {
  // Additional verification required
  await requestAdditionalVerification(paymentIntent);
}

3D Secure

// Require SCA
const paymentIntent = await stripe.paymentIntents.create({
  amount: 10000,
  currency: 'eur',
  payment_method_options: {
    card: {
      request_three_d_secure: 'any'
    }
  }
});

Security Requirements

PCI Compliance

// Never touch card data
// ❌ BAD: Handling card numbers
const cardNumber = req.body.cardNumber;

// ✅ GOOD: Use payment provider's tokenization
const { token } = await stripe.tokens.create({
  card: {
    number: '4242424242424242', // Only in test mode!
    exp_month: 12,
    exp_year: 2030
  }
});

Data Protection

Encrypt sensitive data at rest.

Performance Metrics

MetricTargetCurrent
Checkout Load<1s0.8s
Payment Process<3s2.5s
Webhook Latency<500ms300ms
Success Rate>98%98.5%

FAQ

Q1: Which payment provider?

Stripe for flexibility, Paddle for simplicity, Lemon Squeezy for digital goods.

Q2: Handle international payments?

Support multiple currencies and local payment methods.

Q3: Test webhooks locally?

Use Stripe CLI for webhook forwarding.

Q4: Refund policy?

Automate refunds, track in database.

Q5: Subscription changes?

Allow upgrades/downgrades with proration.

Production Checklist

Before Launch

  • [ ] Test mode payments work
  • [ ] Webhooks configured
  • [ ] Error handling tested
  • [ ] Security audit passed
  • [ ] Load testing done
  • [ ] Monitoring setup
  • [ ] Backup payment method
  • [ ] Legal compliance verified

After Launch

  • Monitor payment success rate
  • Track chargeback rate
  • Review fraud alerts
  • Optimize conversion

Conclusion

Payments are critical infrastructure.

Key takeaways:

  • Never touch card data
  • Always verify webhooks
  • Handle failures gracefully
  • Monitor everything
  • Test thoroughly

Build payment systems that scale.

Resources:

  • Stripe Documentation
  • PCI Compliance Guide
  • Webhook Best Practices
  • Testing Strategies

Next Steps:

  1. Choose payment provider
  2. Implement basic flow
  3. Add webhook handling
  4. Test thoroughly
  5. Launch with monitoring

Accept payments securely today.

Stripe Webhooks: Handle Payment Events Securely