Stripe Webhooks: Handle Payment Events Securely
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
| Metric | Target | Current |
| Checkout Load | <1s | 0.8s |
| Payment Process | <3s | 2.5s |
| Webhook Latency | <500ms | 300ms |
| 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:
- Choose payment provider
- Implement basic flow
- Add webhook handling
- Test thoroughly
- Launch with monitoring
Accept payments securely today.