# Subscription Billing mit Stripe **Meta-Description:** Stripe Subscription Billing implementieren. Checkout, Webhooks, Customer Portal und Metered Billing für SaaS. **Keywords:** Stripe, Subscription, Billing, Webhooks, Customer Portal, Metered Billing, SaaS, Payment --- ## Einführung **Stripe** ist der De-facto-Standard für SaaS Billing. Von einfachen Subscriptions bis komplexem **Metered Billing** – Stripe bietet alle Tools. Dieser Guide zeigt die komplette Implementation für Next.js Anwendungen. --- ## Stripe Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ STRIPE BILLING ARCHITECTURE │ ├─────────────────────────────────────────────────────────────┤ │ │ │ User Flow: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 1. User clicks "Subscribe" │ │ │ │ ↓ │ │ │ │ 2. Create Stripe Customer (if new) │ │ │ │ ↓ │ │ │ │ 3. Create Checkout Session │ │ │ │ ↓ │ │ │ │ 4. Redirect to Stripe Checkout │ │ │ │ ↓ │ │ │ │ 5. User completes payment │ │ │ │ ↓ │ │ │ │ 6. Stripe sends webhook │ │ │ │ ↓ │ │ │ │ 7. Update database, activate subscription │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Key Objects: │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Customer: │ │ │ │ ├── email, name, metadata │ │ │ │ └── payment methods, invoices │ │ │ │ │ │ │ │ Product: │ │ │ │ ├── name, description │ │ │ │ └── has many Prices │ │ │ │ │ │ │ │ Price: │ │ │ │ ├── amount, currency, interval │ │ │ │ └── recurring or one-time │ │ │ │ │ │ │ │ Subscription: │ │ │ │ ├── customer, items (prices) │ │ │ │ ├── status: active, past_due, canceled │ │ │ │ └── billing cycle, trial │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ Webhooks to Handle: │ │ ├── checkout.session.completed │ │ ├── customer.subscription.created │ │ ├── customer.subscription.updated │ │ ├── customer.subscription.deleted │ │ ├── invoice.paid │ │ ├── invoice.payment_failed │ │ └── customer.updated │ │ │ └─────────────────────────────────────────────────────────────┘ ``` --- ## Stripe Setup ```typescript // lib/stripe/client.ts import Stripe from 'stripe'; if (!process.env.STRIPE_SECRET_KEY) { throw new Error('STRIPE_SECRET_KEY is required'); } export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: '2024-06-20', typescript: true }); // lib/stripe/config.ts export const STRIPE_CONFIG = { prices: { pro_monthly: process.env.STRIPE_PRICE_PRO_MONTHLY!, pro_yearly: process.env.STRIPE_PRICE_PRO_YEARLY!, enterprise_monthly: process.env.STRIPE_PRICE_ENTERPRISE_MONTHLY!, enterprise_yearly: process.env.STRIPE_PRICE_ENTERPRISE_YEARLY! }, products: { pro: process.env.STRIPE_PRODUCT_PRO!, enterprise: process.env.STRIPE_PRODUCT_ENTERPRISE! }, webhookSecret: process.env.STRIPE_WEBHOOK_SECRET! }; // Map price IDs to plan names export const PRICE_TO_PLAN: Record = { [STRIPE_CONFIG.prices.pro_monthly]: 'pro', [STRIPE_CONFIG.prices.pro_yearly]: 'pro', [STRIPE_CONFIG.prices.enterprise_monthly]: 'enterprise', [STRIPE_CONFIG.prices.enterprise_yearly]: 'enterprise' }; ``` --- ## Customer Management ```typescript // lib/stripe/customers.ts import { stripe } from './client'; import { db } from '@/lib/db'; export async function getOrCreateStripeCustomer( userId: string ): Promise { // Check if user already has a Stripe customer ID const user = await db.user.findUnique({ where: { id: userId }, select: { stripeCustomerId: true, email: true, name: true } }); if (!user) { throw new Error('User not found'); } if (user.stripeCustomerId) { return user.stripeCustomerId; } // Create new Stripe customer const customer = await stripe.customers.create({ email: user.email!, name: user.name || undefined, metadata: { userId } }); // Save customer ID to database await db.user.update({ where: { id: userId }, data: { stripeCustomerId: customer.id } }); return customer.id; } export async function getCustomerSubscription( customerId: string ): Promise { const subscriptions = await stripe.subscriptions.list({ customer: customerId, status: 'all', limit: 1, expand: ['data.default_payment_method'] }); return subscriptions.data[0] || null; } export async function updateCustomerEmail( customerId: string, email: string ): Promise { await stripe.customers.update(customerId, { email }); } ``` --- ## Checkout Session ```typescript // lib/stripe/checkout.ts import { stripe } from './client'; import { STRIPE_CONFIG } from './config'; import { getOrCreateStripeCustomer } from './customers'; interface CreateCheckoutOptions { userId: string; priceId: string; quantity?: number; trialDays?: number; couponId?: string; metadata?: Record; } export async function createCheckoutSession( options: CreateCheckoutOptions ): Promise { const customerId = await getOrCreateStripeCustomer(options.userId); const sessionConfig: Stripe.Checkout.SessionCreateParams = { customer: customerId, mode: 'subscription', payment_method_types: ['card'], line_items: [ { price: options.priceId, quantity: options.quantity || 1 } ], subscription_data: { trial_period_days: options.trialDays, metadata: { userId: options.userId, ...options.metadata } }, success_url: `${process.env.NEXT_PUBLIC_APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`, allow_promotion_codes: true, billing_address_collection: 'auto', customer_update: { address: 'auto', name: 'auto' } }; // Add coupon if provided if (options.couponId) { sessionConfig.discounts = [{ coupon: options.couponId }]; } const session = await stripe.checkout.sessions.create(sessionConfig); return session.url!; } // API Route // app/api/checkout/route.ts import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { createCheckoutSession } from '@/lib/stripe/checkout'; export async function POST(request: Request) { try { const session = await getServerSession(); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const { priceId, quantity } = await request.json(); const checkoutUrl = await createCheckoutSession({ userId: session.user.id, priceId, quantity, trialDays: 14 }); return NextResponse.json({ url: checkoutUrl }); } catch (error) { console.error('Checkout error:', error); return NextResponse.json( { error: 'Failed to create checkout session' }, { status: 500 } ); } } ``` --- ## Webhook Handler ```typescript // app/api/webhooks/stripe/route.ts import { NextResponse } from 'next/server'; import { headers } from 'next/headers'; import { stripe } from '@/lib/stripe/client'; import { STRIPE_CONFIG, PRICE_TO_PLAN } from '@/lib/stripe/config'; import { db } from '@/lib/db'; import Stripe from 'stripe'; export async function POST(request: Request) { const body = await request.text(); const headersList = await headers(); const signature = headersList.get('stripe-signature')!; let event: Stripe.Event; try { event = stripe.webhooks.constructEvent( body, signature, STRIPE_CONFIG.webhookSecret ); } catch (error) { console.error('Webhook signature verification failed:', error); return NextResponse.json({ error: 'Invalid signature' }, { status: 400 }); } try { switch (event.type) { case 'checkout.session.completed': await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session); break; case 'customer.subscription.created': case 'customer.subscription.updated': await handleSubscriptionChange(event.data.object as Stripe.Subscription); break; case 'customer.subscription.deleted': await handleSubscriptionDeleted(event.data.object as Stripe.Subscription); break; case 'invoice.paid': await handleInvoicePaid(event.data.object as Stripe.Invoice); break; case 'invoice.payment_failed': await handlePaymentFailed(event.data.object as Stripe.Invoice); break; default: console.log(`Unhandled event type: ${event.type}`); } return NextResponse.json({ received: true }); } catch (error) { console.error('Webhook handler error:', error); return NextResponse.json( { error: 'Webhook handler failed' }, { status: 500 } ); } } async function handleCheckoutCompleted(session: Stripe.Checkout.Session) { const customerId = session.customer as string; const subscriptionId = session.subscription as string; // Get user by Stripe customer ID const user = await db.user.findFirst({ where: { stripeCustomerId: customerId } }); if (!user) { throw new Error(`User not found for customer: ${customerId}`); } // Get subscription details const subscription = await stripe.subscriptions.retrieve(subscriptionId); const priceId = subscription.items.data[0].price.id; const plan = PRICE_TO_PLAN[priceId] || 'free'; // Update user subscription await db.user.update({ where: { id: user.id }, data: { stripeSubscriptionId: subscriptionId, plan, subscriptionStatus: subscription.status, currentPeriodEnd: new Date(subscription.current_period_end * 1000) } }); // Send welcome email await sendWelcomeEmail(user.email!, plan); } async function handleSubscriptionChange(subscription: Stripe.Subscription) { const customerId = subscription.customer as string; const user = await db.user.findFirst({ where: { stripeCustomerId: customerId } }); if (!user) return; const priceId = subscription.items.data[0].price.id; const plan = PRICE_TO_PLAN[priceId] || 'free'; await db.user.update({ where: { id: user.id }, data: { plan, subscriptionStatus: subscription.status, currentPeriodEnd: new Date(subscription.current_period_end * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end } }); } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const customerId = subscription.customer as string; const user = await db.user.findFirst({ where: { stripeCustomerId: customerId } }); if (!user) return; await db.user.update({ where: { id: user.id }, data: { plan: 'free', subscriptionStatus: 'canceled', stripeSubscriptionId: null } }); } async function handleInvoicePaid(invoice: Stripe.Invoice) { // Record payment in database if (invoice.subscription) { await db.payment.create({ data: { stripeInvoiceId: invoice.id, stripeCustomerId: invoice.customer as string, amount: invoice.amount_paid, currency: invoice.currency, status: 'paid', paidAt: new Date() } }); } } async function handlePaymentFailed(invoice: Stripe.Invoice) { const customerId = invoice.customer as string; const user = await db.user.findFirst({ where: { stripeCustomerId: customerId } }); if (!user) return; // Update subscription status await db.user.update({ where: { id: user.id }, data: { subscriptionStatus: 'past_due' } }); // Send payment failed email await sendPaymentFailedEmail(user.email!); } ``` --- ## Customer Portal ```typescript // lib/stripe/portal.ts import { stripe } from './client'; export async function createBillingPortalSession( customerId: string, returnUrl?: string ): Promise { const session = await stripe.billingPortal.sessions.create({ customer: customerId, return_url: returnUrl || `${process.env.NEXT_PUBLIC_APP_URL}/settings/billing` }); return session.url; } // API Route // app/api/billing/portal/route.ts import { NextResponse } from 'next/server'; import { getServerSession } from 'next-auth'; import { createBillingPortalSession } from '@/lib/stripe/portal'; import { db } from '@/lib/db'; export async function POST(request: Request) { try { const session = await getServerSession(); if (!session?.user?.id) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } const user = await db.user.findUnique({ where: { id: session.user.id }, select: { stripeCustomerId: true } }); if (!user?.stripeCustomerId) { return NextResponse.json( { error: 'No billing account found' }, { status: 400 } ); } const portalUrl = await createBillingPortalSession(user.stripeCustomerId); return NextResponse.json({ url: portalUrl }); } catch (error) { console.error('Portal error:', error); return NextResponse.json( { error: 'Failed to create portal session' }, { status: 500 } ); } } ``` --- ## Metered Billing ```typescript // lib/stripe/metered.ts import { stripe } from './client'; import { db } from '@/lib/db'; export async function reportUsage( userId: string, quantity: number, idempotencyKey?: string ): Promise { const user = await db.user.findUnique({ where: { id: userId }, select: { stripeSubscriptionId: true } }); if (!user?.stripeSubscriptionId) { throw new Error('User has no active subscription'); } // Get the subscription item for metered billing const subscription = await stripe.subscriptions.retrieve( user.stripeSubscriptionId, { expand: ['items.data.price'] } ); const meteredItem = subscription.items.data.find( item => (item.price as Stripe.Price).recurring?.usage_type === 'metered' ); if (!meteredItem) { throw new Error('No metered billing item found'); } // Report usage await stripe.subscriptionItems.createUsageRecord( meteredItem.id, { quantity, timestamp: Math.floor(Date.now() / 1000), action: 'increment' }, { idempotencyKey } ); } // Get usage for current period export async function getCurrentUsage(userId: string): Promise<{ used: number; limit: number | null; }> { const user = await db.user.findUnique({ where: { id: userId }, select: { stripeSubscriptionId: true } }); if (!user?.stripeSubscriptionId) { return { used: 0, limit: null }; } const subscription = await stripe.subscriptions.retrieve( user.stripeSubscriptionId ); const meteredItem = subscription.items.data.find( item => item.price.recurring?.usage_type === 'metered' ); if (!meteredItem) { return { used: 0, limit: null }; } // Get usage records const usageRecords = await stripe.subscriptionItems.listUsageRecordSummaries( meteredItem.id, { limit: 1 } ); const currentUsage = usageRecords.data[0]?.total_usage || 0; return { used: currentUsage, limit: null // or get from subscription metadata }; } ``` --- ## Subscription Management UI ```tsx // components/billing/SubscriptionStatus.tsx 'use client'; import { useState } from 'react'; import { useSubscription } from '@/hooks/useSubscription'; export function SubscriptionStatus() { const { subscription, isLoading } = useSubscription(); const [isPortalLoading, setIsPortalLoading] = useState(false); const openBillingPortal = async () => { setIsPortalLoading(true); try { const res = await fetch('/api/billing/portal', { method: 'POST' }); const { url } = await res.json(); window.location.href = url; } catch (error) { console.error('Portal error:', error); } setIsPortalLoading(false); }; if (isLoading) return
Loading...
; return (

Subscription

Current Plan {subscription?.plan || 'Free'}
Status
{subscription?.currentPeriodEnd && (
{subscription.cancelAtPeriodEnd ? 'Cancels on' : 'Renews on'} {new Date(subscription.currentPeriodEnd).toLocaleDateString()}
)}
); } function StatusBadge({ status }: { status: string }) { const colors: Record = { active: 'bg-green-100 text-green-800', trialing: 'bg-blue-100 text-blue-800', past_due: 'bg-yellow-100 text-yellow-800', canceled: 'bg-red-100 text-red-800', inactive: 'bg-gray-100 text-gray-800' }; return ( {status.replace('_', ' ')} ); } ``` --- ## Best Practices | Aspect | Recommendation | |--------|----------------| | **Webhooks** | Always verify signatures | | **Idempotency** | Use idempotency keys | | **Retries** | Handle webhook retries | | **Testing** | Use Stripe CLI for local testing | | **Errors** | Log all Stripe errors | | **Security** | Never expose secret keys | --- ## Fazit Stripe Subscription Billing erfordert: 1. **Customer Management**: Stripe Customer ID syncen 2. **Webhooks**: Alle relevanten Events handeln 3. **Portal**: Kunden Self-Service ermöglichen 4. **Metered**: Usage-Based Billing für Flexibilität Robustes Billing ist Grundlage für SaaS-Erfolg. --- ## Bildprompts 1. "Stripe checkout page, credit card form with security badges" 2. "Subscription management dashboard, plan upgrade flow" 3. "Webhook event flow diagram, Stripe to application" --- ## Quellen - [Stripe Billing Documentation](https://stripe.com/docs/billing) - [Stripe Webhooks](https://stripe.com/docs/webhooks) - [Stripe Customer Portal](https://stripe.com/docs/billing/subscriptions/customer-portal) - [Stripe Testing](https://stripe.com/docs/testing)