# 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<string, string> = {
  [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<string> {
  // 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<Stripe.Subscription | null> {
  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<void> {
  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<string, string>;
}

export async function createCheckoutSession(
  options: CreateCheckoutOptions
): Promise<string> {
  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<string> {
  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<void> {
  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 <div>Loading...</div>;

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <h2 className="text-lg font-semibold mb-4">Subscription</h2>

      <div className="space-y-4">
        <div className="flex justify-between">
          <span className="text-gray-600">Current Plan</span>
          <span className="font-semibold capitalize">
            {subscription?.plan || 'Free'}
          </span>
        </div>

        <div className="flex justify-between">
          <span className="text-gray-600">Status</span>
          <StatusBadge status={subscription?.status || 'inactive'} />
        </div>

        {subscription?.currentPeriodEnd && (
          <div className="flex justify-between">
            <span className="text-gray-600">
              {subscription.cancelAtPeriodEnd ? 'Cancels on' : 'Renews on'}
            </span>
            <span>
              {new Date(subscription.currentPeriodEnd).toLocaleDateString()}
            </span>
          </div>
        )}

        <hr />

        <button
          onClick={openBillingPortal}
          disabled={isPortalLoading}
          className="w-full py-2 bg-gray-100 hover:bg-gray-200 rounded-lg"
        >
          {isPortalLoading ? 'Loading...' : 'Manage Subscription'}
        </button>
      </div>
    </div>
  );
}

function StatusBadge({ status }: { status: string }) {
  const colors: Record<string, string> = {
    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 (
    <span className={`px-2 py-1 rounded-full text-sm ${colors[status] || colors.inactive}`}>
      {status.replace('_', ' ')}
    </span>
  );
}
```

---

## 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)
