Introduction
Database
Payments
Webhooks
Handling Stripe webhook events server-side.
All subscription state changes in Launch.now are driven by Stripe webhooks — not by the checkout redirect. This ensures your database stays consistent even when the user closes the browser mid-checkout.
Webhook endpoint
The webhook handler lives at app/api/webhooks/stripe/route.ts:
import { NextRequest, NextResponse } from "next/server";
import { stripe } from "@/lib/stripe";
import { handleStripeEvent } from "@/lib/stripe-events";
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get("stripe-signature")!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch {
return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
}
await handleStripeEvent(event);
return NextResponse.json({ received: true });
}
Handled events
| Event | Action |
|---|---|
checkout.session.completed | Activate subscription, update status to active |
customer.subscription.updated | Sync plan changes, period end date |
customer.subscription.deleted | Mark subscription as canceled |
invoice.payment_succeeded | Extend currentPeriodEnd, send receipt via Inngest |
invoice.payment_failed | Mark subscription as past_due, trigger retry email |
Local webhook testing
Use the Stripe CLI to forward events to your local server:
# Install Stripe CLI, then:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
The CLI outputs a webhook signing secret (whsec_...) — use this as STRIPE_WEBHOOK_SECRET in your .env.local during development.
Never use your production webhook secret in local development. The Stripe CLI generates a separate secret for local forwarding.
Ship your SaaS this week
Auth, billing, orgs, and emails — all wired up. Clone and deploy in minutes.
Get launch.now