DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Stripe Webhooks: The Complete Implementation Guide

Stripe Webhooks: The Complete Implementation Guide

Webhooks are how Stripe tells your server what happened — payment succeeded, subscription cancelled, invoice failed. Getting them wrong loses revenue. Here's the full implementation.

Setup

# Install Stripe CLI for local testing
brew install stripe/stripe-cli/stripe
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# Outputs: Your webhook signing secret is whsec_...
Enter fullscreen mode Exit fullscreen mode

The Webhook Handler

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';

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

export async function POST(request: Request) {
  const body = await request.text(); // RAW body — critical for signature check
  const signature = request.headers.get('stripe-signature')!;

  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return new Response('Invalid signature', { status: 400 });
  }

  // Process event asynchronously
  await handleStripeEvent(event);

  return Response.json({ received: true });
}
Enter fullscreen mode Exit fullscreen mode

Event Handlers

async function handleStripeEvent(event: Stripe.Event) {
  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;
      await handleCheckoutComplete(session);
      break;
    }
    case 'customer.subscription.created':
    case 'customer.subscription.updated': {
      const subscription = event.data.object;
      await syncSubscription(subscription);
      break;
    }
    case 'customer.subscription.deleted': {
      await handleCancellation(event.data.object.customer as string);
      break;
    }
    case 'invoice.payment_failed': {
      await handlePaymentFailed(event.data.object);
      break;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

After Checkout: Grant Access

async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
  const customerId = session.customer as string;
  const priceId = session.line_items?.data[0]?.price?.id;

  // Map price ID to plan name
  const planMap: Record<string, string> = {
    [process.env.STRIPE_PRO_PRICE_ID!]: 'pro',
    [process.env.STRIPE_STARTER_PRICE_ID!]: 'starter',
  };
  const plan = planMap[priceId ?? ''] ?? 'free';

  await db.user.update({
    where: { stripeCustomerId: customerId },
    data: { plan, stripeSubscriptionId: session.subscription as string },
  });
}
Enter fullscreen mode Exit fullscreen mode

Idempotency

// Stripe may deliver the same event multiple times
async function handleStripeEvent(event: Stripe.Event) {
  // Check if already processed
  const existing = await db.processedWebhook.findUnique({
    where: { eventId: event.id }
  });
  if (existing) return;

  // Process
  await processEvent(event);

  // Mark as processed
  await db.processedWebhook.create({
    data: { eventId: event.id, type: event.type }
  });
}
Enter fullscreen mode Exit fullscreen mode

Events to Handle

Event Action
checkout.session.completed Grant access
customer.subscription.updated Update plan
customer.subscription.deleted Downgrade to free
invoice.payment_failed Send dunning email
invoice.payment_succeeded Send receipt

Stripe webhooks ship fully implemented in the AI SaaS Starter Kit — signature verification, idempotency, all key events handled. $99 at whoffagents.com.

Top comments (0)