Stripe Webhooks - Purchase Flow
Complete documentation of what happens when someone purchases your SaaS boilerplate, from payment to account activation.
Overview
When a customer completes a purchase on your platform, Stripe sends webhook events to your server. These webhooks trigger a series of automated processes that handle everything from database updates to welcome emails.
The Complete Purchase Journey
1. Customer Completes Checkout
When someone clicks "Subscribe" and completes payment on Stripe Checkout:
- Stripe processes the payment and creates a checkout session
- Webhook triggered:
checkout.session.completed - Your server receives the payment confirmation instantly
2. Account Activation Process
// What happens in handleCheckoutCompleted()
async function handleCheckoutCompleted(session) {
// Extract customer info from Stripe
const customerEmail = session.customer_email
const userId = session.metadata?.user_id
// Update user in your database
await supabaseAdmin
.from('user_profiles')
.update({
stripe_customer_id: session.customer,
subscription_status: 'active',
has_access: true, // ← Customer gets immediate access
updated_at: new Date()
})
.eq('id', userId)
// Send welcome email automatically
await sendWelcomeEmail(customerEmail, paymentDetails)
}What the customer experiences:
- Payment completes on Stripe
- Within seconds, their account status changes to "active"
- They receive a welcome email with payment confirmation
- They can immediately access premium features
3. Subscription Management Setup
For recurring subscriptions, Stripe also triggers customer.subscription.created:
async function handleSubscriptionCreated(subscription) {
// Find the customer in your database
const customer = await stripe.customers.retrieve(subscription.customer)
const userProfile = await getUserByEmail(customer.email)
// Create subscription record
await supabaseAdmin
.from('user_subscriptions')
.upsert({
stripe_subscription_id: subscription.id,
user_id: userProfile.id,
plan_id: getPlanFromStripePrice(subscription.items.data[0].price.id),
status: subscription.status,
current_period_start: new Date(subscription.current_period_start * 1000),
current_period_end: new Date(subscription.current_period_end * 1000)
})
}Database gets updated with:
- Subscription ID for future reference
- Billing cycle dates
- Plan details
- Payment status
Email Notifications System
Welcome Email (Immediate)
Sent right after payment completion:
const welcomeEmail = {
subject: '🎉 Welcome to our platform! Payment confirmed',
content: `
Hello ${customerName},
Thank you for your payment of $${amount} USD.
Your account is now active!
You can now access all premium features.
`
}Subscription Confirmation (For recurring plans)
Additional email for subscription-based purchases:
const subscriptionEmail = {
subject: '📋 Your subscription has been activated',
content: `
Your ${planName} subscription has been successfully activated
for $${amount} USD/month.
Next billing date: ${nextBillingDate}
`
}What Happens During Billing Cycles
Monthly Renewals
Every month, Stripe automatically:
- Charges the customer's card
- Sends webhook:
invoice.payment_succeeded - Your system processes it:
async function handlePaymentSucceeded(invoice) {
// Send payment confirmation
await sendPaymentSuccessEmail(customer.email, {
amount: invoice.amount_paid / 100,
invoiceUrl: invoice.hosted_invoice_url,
paidAt: new Date(invoice.status_transitions.paid_at * 1000)
})
}- Customer receives payment confirmation email
- Access continues uninterrupted
Failed Payments
If a payment fails:
- Stripe sends:
invoice.payment_failed - Your system responds:
async function handlePaymentFailed(invoice) {
// Update user status
await supabaseAdmin
.from('user_profiles')
.update({
subscription_status: 'past_due',
has_access: false // ← Access gets revoked
})
.eq('id', customer.metadata.user_id)
}- Customer loses access to premium features
- Stripe handles dunning management (retry attempts)
Subscription Lifecycle Events
Customer Cancels Subscription
When someone cancels their subscription:
- Webhook received:
customer.subscription.updated(with cancel_at_period_end = true) - Database updated with cancellation info
- Access maintained until period end
- Cancellation email sent:
const cancellationEmail = {
subject: '🗑️ Your subscription has been canceled',
content: `
Your ${planName} subscription has been canceled.
You'll continue to have access until ${endDate}.
We're sad to see you go!
`
}Subscription Expires
At the end of the billing period:
- Webhook:
customer.subscription.deleted - Access revoked immediately
- Database updated to reflect cancellation
- Final email sent confirming end of service
Real-World Example Flow
Let's say John purchases your $29/month Pro plan:
Minute 0: Payment
- John completes Stripe Checkout
- Payment of $29 processes successfully
Minute 0:15: Webhook Processing
- Your server receives
checkout.session.completed - John's account status → "active"
- John's
has_access→ true
Minute 0:30: Welcome Email
- John receives welcome email with payment confirmation
- Email includes access instructions and next steps
Minute 1: Subscription Setup
customer.subscription.createdwebhook processed- Subscription record created in database
- John's billing cycle starts (next charge: 30 days)
Day 30: First Renewal
- Stripe charges John's card automatically
invoice.payment_succeededwebhook- John gets payment confirmation email
- Service continues uninterrupted
Database Schema Integration
The webhook system integrates with these database tables:
user_profiles
-- Updated on every purchase/cancellation
subscription_status: 'active' | 'canceled' | 'past_due'
has_access: boolean
stripe_customer_id: stringuser_subscriptions
-- Created/updated for subscription events
stripe_subscription_id: string
plan_id: uuid
status: string
current_period_start: timestamp
current_period_end: timestamp
canceled_at: timestamp (nullable)purchases (if using one-time payments)
-- Created for completed checkouts
stripe_session_id: string
amount: integer
currency: string
status: 'completed'Error Handling & Reliability
Webhook Verification
Every webhook is verified using Stripe signatures to prevent fraud:
// Webhook signature verification
const stripeEvent = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET
)Graceful Error Handling
If something fails during processing:
- Errors are logged but don't crash the system
- Stripe will retry failed webhooks automatically
- Customer access is never left in limbo
Idempotency
Webhooks can be received multiple times, but your system handles duplicates gracefully:
- Database operations use
upsertinstead ofinsert - Email sending includes deduplication logic
- Status updates are idempotent
Testing Your Webhook Flow
Local Development
Use Stripe CLI to test webhooks locally:
# Listen for webhooks
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Trigger test events
stripe trigger checkout.session.completed
stripe trigger customer.subscription.created
stripe trigger invoice.payment_failedProduction Verification
Monitor webhook delivery in your Stripe Dashboard:
- Check webhook endpoint status
- Review failed webhook attempts
- Monitor response times and success rates
Security Considerations
Webhook Endpoint Protection
- Always verify webhook signatures
- Use HTTPS endpoints only
- Implement rate limiting
- Log all webhook events for audit trails
Customer Data Handling
- Sensitive data is never logged in plain text
- Email addresses are masked in logs
- Payment details are handled by Stripe, not stored locally
The webhook system ensures that every purchase is processed reliably, customers get immediate access to what they paid for, and the entire billing lifecycle is automated. This creates a smooth experience where customers can purchase and start using your boilerplate within seconds.