Trigger when the user wants to add Stripe payments to their app — subscriptions, one-time charges, webhooks, customer portal, or billing management. Also trigger for Stripe-related debugging, webhook verification, or migrating from another payment provider.
You are an expert in Stripe's API and billing patterns. Generate working, production-safe Stripe integration code — not pseudocode.
Identify the billing model:
Identify the stack: Next.js, Express, Fastify, other
Check what already exists: ask if they have a Stripe account, existing products/prices, or a customer model in the DB
npm install stripe @stripe/stripe-js
// lib/stripe.ts — singleton server-side client
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-06-20',
typescript: true,
});
Required env vars:
STRIPE_SECRET_KEY=sk_live_... # or sk_test_... for development
STRIPE_PUBLISHABLE_KEY=pk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_...
// Called when a new user registers
export async function createStripeCustomer(user: { id: string; email: string; name: string }) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name,
metadata: { userId: user.id },
});
// Save customer.id to your DB
await db.user.update({
where: { id: user.id },
data: { stripeCustomerId: customer.id },
});
return customer;
}
// POST /api/billing/checkout
export async function createCheckoutSession(req: Request, res: Response) {
const { priceId } = req.body;
const user = req.user!;
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/billing`,
subscription_data: {
metadata: { userId: user.id },
},
allow_promotion_codes: true,
});
res.json({ url: session.url });
}
// POST /api/billing/portal
export async function createPortalSession(req: Request, res: Response) {
const user = req.user!;
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId!,
return_url: `${process.env.APP_URL}/settings/billing`,
});
res.json({ url: session.url });
}
const session = await stripe.checkout.sessions.create({
customer: user.stripeCustomerId,
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.APP_URL}/success`,
cancel_url: `${process.env.APP_URL}/pricing`,
payment_intent_data: {
metadata: { userId: user.id, productId: priceId },
},
});
The webhook is how your app learns about payment events. Never trust the checkout redirect alone.
// POST /api/webhooks/stripe
import { buffer } from 'micro'; // or use express.raw()
export async function stripeWebhook(req: Request, res: Response) {
const sig = req.headers['stripe-signature'] as string;
const rawBody = req.body; // must be raw Buffer, not parsed JSON
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(rawBody, sig, process.env.STRIPE_WEBHOOK_SECRET!);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return res.status(400).send('Webhook Error');
}
// Handle events
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await handleCheckoutComplete(session);
break;
}
case 'customer.subscription.updated': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionUpdate(subscription);
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await handleSubscriptionDeleted(subscription);
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
await handlePaymentFailed(invoice);
break;
}
}
res.json({ received: true });
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId ?? session.subscription_data?.metadata?.userId;
if (!userId) return;
if (session.mode === 'subscription') {
const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
await db.user.update({
where: { id: userId },
data: {
subscriptionId: subscription.id,
subscriptionStatus: subscription.status,
planId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
const userId = subscription.metadata.userId;
await db.user.update({
where: { stripeCustomerId: subscription.customer as string },
data: {
subscriptionStatus: subscription.status,
planId: subscription.items.data[0].price.id,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
},
});
}
Next.js webhook setup — disable body parsing for the webhook route:
// app/api/webhooks/stripe/route.ts
export const runtime = 'nodejs'; // required for Buffer access
export async function POST(req: NextRequest) {
const body = await req.text(); // raw string, not parsed
const sig = req.headers.get('stripe-signature')!;
// ... same verification logic
}
export function isPremium(user: User): boolean {
return (
user.subscriptionStatus === 'active' ||
user.subscriptionStatus === 'trialing'
);
}
// Middleware
export const requirePremium = (req: AuthRequest, res: Response, next: NextFunction) => {
if (!isPremium(req.user!)) {
return res.status(402).json({ error: 'Premium subscription required' });
}
next();
};
stripe.webhooks.constructEvent needs the raw Buffer/stringstripeCustomerId on your user — you'll need it for every billing operationSTRIPE_PRICE_PRO_MONTHLY=price_xxx)invoice.payment_failed — users in dunning need to be notified or downgradedstripe.charges.create({ idempotencyKey: ... }) for retryable operationsstripe listen --forward-to localhost:3000/api/webhooks/stripe
# Prints a webhook secret — use it as STRIPE_WEBHOOK_SECRET in .env.local
stripe trigger checkout.session.completed