Stripe Integration Helper
Assist with Stripe payment gateway integration for SaaS applications.
Quick Reference
Installation
bun add stripe @stripe/stripe-js
Environment Variables
Server-side (secret)
STRIPE_SECRET_KEY="sk_live_..." STRIPE_WEBHOOK_SECRET="whsec_..."
Client-side (publishable)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_live_..."
App URL for callbacks
NEXT_PUBLIC_APP_URL="https://your-app.com"
SDK Initialization
Server-side:
// lib/stripe.ts import Stripe from "stripe";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { apiVersion: "2025-01-27.acacia", typescript: true, });
Client-side:
// lib/stripe-client.ts import { loadStripe } from "@stripe/stripe-js";
export const stripePromise = loadStripe( process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY! );
Common Tasks
- Create Checkout Session
// app/api/checkout/route.ts import { NextRequest, NextResponse } from "next/server"; import { auth } from "@clerk/nextjs/server"; import { stripe } from "@/lib/stripe";
export async function POST(request: NextRequest) { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
const { priceId } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card"],
line_items: [{ price: priceId, quantity: 1 }],
success_url: ${process.env.NEXT_PUBLIC_APP_URL}/billing/success?session_id={CHECKOUT_SESSION_ID},
cancel_url: ${process.env.NEXT_PUBLIC_APP_URL}/pricing,
metadata: { userId },
customer_email: user.email, // Optional: pre-fill email
});
return NextResponse.json({ url: session.url }); }
- Create Customer Portal Session
// app/api/billing/portal/route.ts export async function POST(request: NextRequest) { const { userId } = await auth(); if (!userId) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
// Get Stripe customer ID from your database const user = await db.query.users.findFirst({ where: eq(users.id, userId), });
if (!user?.stripeCustomerId) { return NextResponse.json({ error: "No subscription" }, { status: 400 }); }
const session = await stripe.billingPortal.sessions.create({
customer: user.stripeCustomerId,
return_url: ${process.env.NEXT_PUBLIC_APP_URL}/dashboard,
});
return NextResponse.json({ url: session.url }); }
- Webhook Handler
// app/api/webhooks/stripe/route.ts import { NextRequest, NextResponse } from "next/server"; import { stripe } from "@/lib/stripe"; import Stripe from "stripe";
export async function POST(request: NextRequest) { const body = await request.text(); 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"); return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); }
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 handleSubscriptionCancelled(subscription); break; } case "invoice.payment_failed": { const invoice = event.data.object as Stripe.Invoice; await handlePaymentFailed(invoice); break; } }
return NextResponse.json({ received: true }); }
Webhook Events
Event When to Handle
checkout.session.completed
User completes checkout
customer.subscription.created
New subscription starts
customer.subscription.updated
Plan change, renewal
customer.subscription.deleted
Subscription cancelled
invoice.payment_succeeded
Successful payment
invoice.payment_failed
Failed payment attempt
customer.updated
Customer info changed
Subscription Status Values
Status Description
active
Subscription is current
past_due
Payment failed, retrying
canceled
Subscription ended
unpaid
All retry attempts failed
trialing
In trial period
incomplete
First payment pending
Database Schema
Users Table (add Stripe fields)
stripeCustomerId TEXT UNIQUE stripeSubscriptionId TEXT stripePriceId TEXT stripeCurrentPeriodEnd TIMESTAMP
Subscription Sync Pattern
async function syncSubscription( userId: string, subscription: Stripe.Subscription ) { await db .update(users) .set({ stripeSubscriptionId: subscription.id, stripePriceId: subscription.items.data[0].price.id, stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), }) .where(eq(users.id, userId)); }
Feature Gating
async function checkFeatureAccess(userId: string): Promise<boolean> { const user = await db.query.users.findFirst({ where: eq(users.id, userId), });
if (!user?.stripeSubscriptionId) return false;
// Check if subscription is still valid const now = new Date(); return user.stripeCurrentPeriodEnd > now; }
Testing
Test Card Numbers
Card Scenario
4242424242424242
Successful payment
4000000000000002
Card declined
4000002500003155
Requires 3D Secure
4000000000009995
Insufficient funds
Stripe CLI for Local Webhooks
Install Stripe CLI
brew install stripe/stripe-cli/stripe
Login
stripe login
Forward webhooks to local
stripe listen --forward-to localhost:3000/api/webhooks/stripe
Trigger test events
stripe trigger checkout.session.completed
Pricing Page Pattern
// Get prices from Stripe const prices = await stripe.prices.list({ active: true, expand: ["data.product"], });
// Display in component {prices.data.map((price) => ( <PriceCard key={price.id} name={(price.product as Stripe.Product).name} price={price.unit_amount! / 100} interval={price.recurring?.interval} priceId={price.id} /> ))}
Security Best Practices
-
Never expose secret key - Use STRIPE_SECRET_KEY only server-side
-
Verify webhook signatures - Always use stripe.webhooks.constructEvent
-
Idempotency - Store event IDs to prevent duplicate processing
-
Raw body for webhooks - Don't parse JSON before verification
-
Use metadata - Store userId in checkout session metadata
Common Issues
Issue Solution
Webhook signature invalid Use raw body, not parsed JSON
Customer not found Create customer before checkout
Subscription not syncing Check webhook event registration
Test cards failing Ensure using test mode keys
Portal not loading Verify customer has active subscription
Useful Commands
List products
stripe products list
List prices
stripe prices list
Get subscription
stripe subscriptions retrieve sub_xxx
Cancel subscription
stripe subscriptions cancel sub_xxx