Stripe Client Setup
```typescript
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
typescript: true,
});
export const PRICES = {
free: process.env.STRIPE_PRICE_FREE!,
pro: process.env.STRIPE_PRICE_PRO!,
enterprise: process.env.STRIPE_PRICE_ENTERPRISE!,
} as const;
export type PriceTier = keyof typeof PRICES;
```
Create Checkout Session
```typescript
// api/create-checkout.ts
import { stripe, PRICES, PriceTier } from '@/lib/stripe';
interface CreateCheckoutParams {
userId: string;
email: string;
tier: PriceTier;
successUrl: string;
cancelUrl: string;
}
export async function createCheckoutSession({
userId,
email,
tier,
successUrl,
cancelUrl,
}: CreateCheckoutParams): Promise {
// Get or create Stripe customer
let customer = await getStripeCustomer(userId);
if (!customer) {
customer = await stripe.customers.create({
email,
metadata: { userId },
});
await saveStripeCustomerId(userId, customer.id);
}
const session = await stripe.checkout.sessions.create({
customer: customer.id,
mode: 'subscription',
payment_method_types: ['card'],
line_items: [
{
price: PRICES[tier],
quantity: 1,
},
],
success_url: ${successUrl}?session_id={CHECKOUT_SESSION_ID},
cancel_url: cancelUrl,
subscription_data: {
metadata: { userId },
},
allow_promotion_codes: true,
});
return session.url!;
}
```
Webhook Handler
```typescript
// api/webhooks/stripe.ts
import { stripe } from '@/lib/stripe';
import { headers } from 'next/headers';
const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
export async function POST(req: Request) {
const body = await req.text();
const signature = headers().get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, WEBHOOK_SECRET);
} catch (err) {
console.error('Webhook signature verification failed:', err);
return new Response('Invalid signature', { status: 400 });
}
try {
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutComplete(event.data.object);
break;
case 'customer.subscription.updated':
await handleSubscriptionUpdate(event.data.object);
break;
case 'customer.subscription.deleted':
await handleSubscriptionCanceled(event.data.object);
break;
case 'invoice.payment_failed':
await handlePaymentFailed(event.data.object);
break;
default:
console.log(Unhandled event type: ${event.type});
}
return new Response('OK', { status: 200 });
} catch (err) {
console.error('Webhook handler error:', err);
return new Response('Webhook handler failed', { status: 500 });
}
}
async function handleCheckoutComplete(session: Stripe.Checkout.Session) {
const userId = session.metadata?.userId;
const subscriptionId = session.subscription as string;
if (!userId || !subscriptionId) return;
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
const priceId = subscription.items.data[0]?.price.id;
const tier = Object.entries(PRICES).find(([, id]) => id === priceId)?.[0] || 'free';
await updateUserSubscription(userId, {
stripeSubscriptionId: subscriptionId,
stripeCustomerId: session.customer as string,
tier,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
});
}
async function handleSubscriptionUpdate(subscription: Stripe.Subscription) {
const userId = subscription.metadata?.userId;
if (!userId) return;
const priceId = subscription.items.data[0]?.price.id;
const tier = Object.entries(PRICES).find(([, id]) => id === priceId)?.[0] || 'free';
await updateUserSubscription(userId, {
tier,
status: subscription.status,
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
cancelAtPeriodEnd: subscription.cancel_at_period_end,
});
}
async function handleSubscriptionCanceled(subscription: Stripe.Subscription) {
const userId = subscription.metadata?.userId;
if (!userId) return;
await updateUserSubscription(userId, {
tier: 'free',
status: 'canceled',
stripeSubscriptionId: null,
});
}
async function handlePaymentFailed(invoice: Stripe.Invoice) {
const customerId = invoice.customer as string;
const user = await getUserByStripeCustomerId(customerId);
if (user) {
await sendPaymentFailedEmail(user.email, {
amount: invoice.amount_due / 100,
nextRetry: invoice.next_payment_attempt
? new Date(invoice.next_payment_attempt * 1000)
: null,
});
}
}
```
Customer Portal
```typescript
// api/create-portal.ts
export async function createPortalSession(userId: string): Promise {
const user = await getUser(userId);
if (!user?.stripeCustomerId) {
throw new Error('No Stripe customer found');
}
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: ${process.env.NEXT_PUBLIC_URL}/settings/billing,
});
return session.url;
}
```