stripe
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
1. 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 });
}
2. 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 });
}
3. 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_KEYonly 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
More from andrehfp/tinyplate
abacatepay
Help with AbacatePay payment integration in Next.js projects. Use when implementing PIX payments, managing subscriptions, handling webhooks, or debugging payment flows. Covers SDK usage, webhook verification, and billing management for Brazilian SaaS applications.
42marketing-copy
Generate compelling marketing copy using the Elevated Direct Response framework. Use this skill when creating landing pages, headlines, email campaigns, ad copy, CTAs, value propositions, or any persuasive marketing content. Applies contrarian educator tone and direct response principles.
37seo-technical
Implement technical SEO infrastructure for Next.js apps. Use this skill when setting up sitemaps, robots.txt, meta tags, OpenGraph, structured data (JSON-LD), canonical URLs, and other technical SEO elements. Covers Next.js 15/16 App Router patterns and 2025 best practices.
37posthog
Implement PostHog analytics, feature flags, and session replay for Next.js apps. Use this skill for event tracking, user identification, A/B testing, experiments, and session recording setup. Also handles analytics reporting (funnel analysis, retention, SEO) with Google Search Console integration.
27ux-design
Design intuitive user experiences following Jobs-era Apple principles. Use this skill when designing onboarding flows, empty states, dashboards, user journeys, CTAs, forms, or any UI that needs to anticipate user needs and reduce friction. Applies progressive disclosure, anticipatory design, and conversion optimization principles.
18favicon
Generate favicons and app icons for Next.js projects. Creates all required sizes, formats, and configures metadata.
12