Skip to content

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

typescript
// 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:

typescript
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:

typescript
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:

typescript
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:

  1. Charges the customer's card
  2. Sends webhook: invoice.payment_succeeded
  3. Your system processes it:
typescript
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)
  })
}
  1. Customer receives payment confirmation email
  2. Access continues uninterrupted

Failed Payments

If a payment fails:

  1. Stripe sends: invoice.payment_failed
  2. Your system responds:
typescript
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)
}
  1. Customer loses access to premium features
  2. Stripe handles dunning management (retry attempts)

Subscription Lifecycle Events

Customer Cancels Subscription

When someone cancels their subscription:

  1. Webhook received: customer.subscription.updated (with cancel_at_period_end = true)
  2. Database updated with cancellation info
  3. Access maintained until period end
  4. Cancellation email sent:
typescript
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:

  1. Webhook: customer.subscription.deleted
  2. Access revoked immediately
  3. Database updated to reflect cancellation
  4. 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.created webhook 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_succeeded webhook
  • John gets payment confirmation email
  • Service continues uninterrupted

Database Schema Integration

The webhook system integrates with these database tables:

user_profiles

sql
-- Updated on every purchase/cancellation
subscription_status: 'active' | 'canceled' | 'past_due'
has_access: boolean
stripe_customer_id: string

user_subscriptions

sql
-- 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)

sql
-- 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:

typescript
// 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 upsert instead of insert
  • Email sending includes deduplication logic
  • Status updates are idempotent

Testing Your Webhook Flow

Local Development

Use Stripe CLI to test webhooks locally:

bash
# 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_failed

Production 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.

Built with love by mhdevfr