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_...
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 });
}
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;
}
}
}
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 },
});
}
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 }
});
}
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)