Stripe Integration Expert
helps developers implement production-grade Stripe integrations for various billing models, including subscriptions, one-time payments, and usage-based billing.
Install
npx promptshop add stripe-integration-expertDetails
What This Skill Does
This skill helps developers implement production-grade Stripe integrations for various billing models, including subscriptions, one-time payments, and usage-based billing. It provides patterns for Next.js, Express, and Django, and covers essential features like webhook handling and customer portals. This is ideal for engineering teams building robust payment systems.
When to Use
Adding subscription billing to a web app Implementing plan upgrades/downgrades Building usage-based billing systems Debugging webhook delivery failures Migrating between billing models Setting up local Stripe testing
Key Features
Subscription lifecycle management Trial handling and conversion tracking Proration calculation and credit application Idempotent webhook handlers Customer portal integration Full Stripe CLI local testing setup Tier: POWERFUL Category: Engineering Team Domain: Payments / Billing Infrastructure
Overview
Implement production-grade Stripe integrations: subscriptions with trials and proration, one-time payments, usage-based billing, checkout sessions, idempotent webhook handlers, customer portal, and invoicing. Covers Next.js, Express, and Django patterns.
Core Capabilities
Subscription lifecycle management (create, upgrade, downgrade, cancel, pause) Trial handling and conversion tracking Proration calculation and credit application Usage-based billing with metered pricing Idempotent webhook handlers with signature verification Customer portal integration Invoice generation and PDF access Full Stripe CLI local testing setup
When to Use
Adding subscription billing to any web app Implementing plan upgrades/downgrades with proration Building usage-based or seat-based billing Debugging webhook delivery failures Migrating from one billing model to another
Subscription Lifecycle State Machine
FREE_TRIAL ──paid──► ACTIVE ──cancel──► CANCEL_PENDING ──period_end──► CANCELED │ │ │ │ downgrade reactivate │ ▼ │ │ DOWNGRADING ──period_end──► ACTIVE (lower plan) │ │ │ └──trial_end without payment──► PAST_DUE ──payment_failed 3x──► CANCELED │ payment_success │ ▼ ACTIVE
DB subscription status values:
trialing | active | past_due | canceled | cancel_pending | paused | unpaid
Stripe Client Setup
// lib/stripe.ts import Stripe from "stripe" export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { api Version: "2024-04-10", typescript: true, app Info: { name: "myapp", version: "1.0.0", }, }) // Price IDs by plan (set in env) export const PLANS = { starter: { monthly: process.env.STRIPE_STARTER_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_STARTER_YEARLY_PRICE_ID!, features: ["5 projects", "10k events"], }, pro: { monthly: process.env.STRIPE_PRO_MONTHLY_PRICE_ID!, yearly: process.env.STRIPE_PRO_YEARLY_PRICE_ID!, features: ["Unlimited projects", "1M events"], }, } as const
Checkout Session (Next.js App Router)
// app/api/billing/checkout/route.ts import { Next Response } from "next/server" import { stripe } from "@/lib/stripe" import { get Auth User } from "@/lib/auth" import { db } from "@/lib/db" export async function POST(req: Request) { const user = await get Auth User() if (!user) return Next Response.json({ error: "Unauthorized" }, { status: 401 }) const { price Id, interval = "monthly" } = await req.json() // Get or create Stripe customer let stripe Customer Id = user.stripe Customer Id if (!stripe Customer Id) { const customer = await stripe.customers.create({ email: user.email, name: "username-undefined" metadata: { user Id: user.id }, }) stripe Customer Id = customer.id await db.user.update({ where: { id: user.id }, data: { stripe Customer Id } }) } const session = await stripe.checkout.sessions.create({ customer: stripe Customer Id, mode: "subscription", payment_method_types: ["card"], line_items: [{ price: price Id, quantity: 1 }], allow_promotion_codes: true, subscription_data: { trial_period_days: user.has Had Trial ? undefined : 14, metadata: { user Id: user.id }, }, success_url: ${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}, cancel_url: ${process.env.NEXT_PUBLIC_APP_URL}/pricing, metadata: { user Id: user.id }, }) return Next Response.json({ url: session.url }) }
Subscription Upgrade/Downgrade
// lib/billing.ts export async function change Subscription Plan( subscription Id: string, new Price Id: string, immediate = false ) { const subscription = await stripe.subscriptions.retrieve(subscription Id) const current Item = subscription.items.data[0] if (immediate) { // Upgrade: apply immediately with proration return stripe.subscriptions.update(subscription Id, { items: [{ id: current Item.id, price: new Price Id }], proration_behavior: "always_invoice", billing_cycle_anchor: "unchanged", }) } else { // Downgrade: apply at period end, no proration return stripe.subscriptions.update(subscription Id, { items: [{ id: current Item.id, price: new Price Id }], proration_behavior: "none", billing_cycle_anchor: "unchanged", }) } } // Preview proration before confirming upgrade export async function preview Proration(subscription Id: string, new Price Id: string) { const subscription = await stripe.subscriptions.retrieve(subscription Id) const proration Date = Math.floor(Date.now() / 1000) const invoice = await stripe.invoices.retrieve Upcoming({ customer: subscription.customer as string, subscription: subscription Id, subscription_items: [{ id: subscription.items.data[0].id, price: new Price Id }], subscription_proration_date: proration Date, }) return { amount Due: invoice.amount_due, proration Date, line Items: invoice.lines.data, } }
Complete Webhook Handler (Idempotent)
// app/api/webhooks/stripe/route.ts import { Next Response } from "next/server" import { headers } from "next/headers" import { stripe } from "@/lib/stripe" import { db } from "@/lib/db" import Stripe from "stripe" // Processed events table to ensure idempotency async function has Processed Event(event Id: string): Promise<boolean> { const existing = await db.stripe Event.find Unique({ where: { id: event Id } }) return !!existing } async function mark Event Processed(event Id: string, type: string) { await db.stripe Event.create({ data: { id: event Id, type, processed At: new Date() } }) } 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.construct Event(body, signature, process.env.STRIPE_WEBHOOK_SECRET!) } catch (err) { console.error("Webhook signature verification failed:", err) return Next Response.json({ error: "Invalid signature" }, { status: 400 }) } // Idempotency check if (await has Processed Event(event.id)) { return Next Response.json({ received: true, skipped: true }) } try { switch (event.type) { case "checkout.session.completed":
- await handle Checkout Completed(event.data.object as Stripe.
- Checkout.
- Session).
break case "customer.subscription.created": case "customer.subscription.updated": await handle Subsc