Stripe Customer PortalAbos, Webhooks und Next.js 2026

stripe5 Min. Lesezeit20. Juli 2026

Autor: DevStudio.it

TL;DR

Das Stripe Customer Portal ist eine von Stripe gehostete Oberfläche, in der Kunden Pläne wechseln, Karten aktualisieren, Rechnungen laden und Abos kündigen — ohne eigenes Billing-Panel. In Next.js erstellen Sie serverseitig eine Portal-Session, synchronisieren den Abo-Status per Webhooks und speichern customerId in Branchly. Architektur, Route-Handler-Code, Webhook-Events und Produktions-Checkliste auf DevStudioIT Cloud folgen.

Für wen

  • SaaS-Gründer mit Abo-Modell (monatlich / jährlich)
  • Next.js-Entwickler, die Billing nach Checkout Session anbinden
  • Product Owner mit Self-Service-Kündigung (regulatorische Erwartung in der EU)
  • Teams mit Stripe Checkout, aber ohne „Mein Konto → Zahlungen“

Keyword (SEO)

stripe customer portal nextjs, stripe abo webhooks, billing portal saas 2026, abo kündigen self-service

Customer Portal vs eigenes Panel — wann was

Aspekt Customer Portal Eigenes UI + Stripe API
Time-to-Market Stunden Wochen
Karte, Rechnungs-PDF Eingebaut Selbst bauen
Plan Upgrade/Downgrade Dashboard-Konfig Eigene Logik + Proration
Branding Stripe-Logo, Farben Volle Kontrolle
Lokalisierung Viele Sprachen OOTB Eigene Übersetzungen

Für MVP und mittleres SaaS gewinnt das Portal. Eigenes Panel bei komplexen Seats, Usage-Based Billing mit Charts oder ERP-Anbindung.

Architektur in Next.js

Typischer Ablauf:

  1. Nutzer meldet sich an (NextAuth, Clerk, eigenes JWT)
  2. In Branchly (branchly.cloud) liegen users.stripeCustomerId und subscriptions.status
  3. Klick „Abo verwalten“ → POST /api/billing/portal
  4. Server: stripe.billingPortal.sessions.create({ customer, return_url })
  5. Redirect zur Stripe-Session → Kunde bearbeitet → Rückkehr zu return_url
  6. Webhook customer.subscription.updated aktualisiert Branchly

App auf DevStudioIT Cloud (devstudioit.cloud): Webhook-Endpoint öffentlich per HTTPS mit Signaturprüfung — Secrets in env, nicht im Repo.

Konfiguration im Stripe Dashboard

Unter Settings → Billing → Customer portal aktivieren:

  • Update payment method
  • Cancel subscription (sofort oder Periodenende — Produktpolitik)
  • Switch plans (bei mehreren Price IDs)
  • Invoice history

Products / Prices für Planwechsel zuweisen. Testmodus mit Karte 4242 4242 4242 4242.

Route Handler — Portal-Session

// app/api/billing/portal/route.ts
import Stripe from 'stripe';
import { NextResponse } from 'next/server';
import { getSession } from '@/lib/auth';
import { db } from '@/lib/db'; // Branchly client

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST() {
  const session = await getSession();
  if (!session?.userId) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const user = await db.user.findUnique({
    where: { id: session.userId },
    select: { stripeCustomerId: true },
  });

  if (!user?.stripeCustomerId) {
    return NextResponse.json({ error: 'No billing account' }, { status: 400 });
  }

  const portalSession = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.APP_URL}/de/konto/abonnement`,
  });

  return NextResponse.json({ url: portalSession.url });
}

Front-end: window.location.href = data.url — kein iframe; Stripe verlangt vollen Redirect.

Webhooks — zu behandelnde Events

Minimum für Abos:

Event Aktion in Branchly
checkout.session.completed customerId, subscriptionId, Plan speichern
customer.subscription.created Status active, current_period_end
customer.subscription.updated Neuer Plan, past_due, Pause
customer.subscription.deleted Status canceled, Feature-Zugang entziehen
invoice.payment_failed E-Mail + Grace Period in App
invoice.paid Zugang freigeben, Rechnung loggen
// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { headers } from 'next/headers';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;

export async function POST(req: Request) {
  const body = await req.text();
  const sig = headers().get('stripe-signature')!;
  let event: Stripe.Event;

  try {
    event = stripe.webhooks.constructEvent(body, sig, webhookSecret);
  } catch {
    return new Response('Invalid signature', { status: 400 });
  }

  switch (event.type) {
    case 'customer.subscription.updated':
    case 'customer.subscription.deleted': {
      const sub = event.data.object as Stripe.Subscription;
      await syncSubscriptionToBranchly(sub);
      break;
    }
    // ... weitere cases
  }

  return new Response('ok', { status: 200 });
}

Idempotenz: event.id in Tabelle stripe_webhook_events — Stripe retried bis 72 h.

Abo-Status in der App synchronisieren

Middleware oder Server Component prüft vor /dashboard:

const sub = await db.subscription.findFirst({
  where: { userId, status: { in: ['active', 'trialing'] } },
});
if (!sub) redirect('/de/preise');

Nicht nur auf Wochen altes JWT verlassen — Source of Truth: Webhook + tägliches Reconcile (stripe.subscriptions.retrieve vs Branchly).

Checkout → Portal — ein Customer

Beim ersten Checkout customer_creation: 'always' oder customer_email auf existierenden Stripe Customer mappen — keine Duplikate, kein kaputtes Portal.

const checkoutSession = await stripe.checkout.sessions.create({
  mode: 'subscription',
  customer: existingStripeCustomerId ?? undefined,
  customer_email: existingStripeCustomerId ? undefined : user.email,
  line_items: [{ price: priceId, quantity: 1 }],
  success_url: `${APP_URL}/de/konto?success=1`,
  cancel_url: `${APP_URL}/de/preise`,
});

Testen und Go-live

  1. Stripe CLI: stripe listen --forward-to localhost:3000/api/webhooks/stripe
  2. Plan-Upgrade im Portal testen → Webhook und UI prüfen
  3. Kündigung „zum Periodenende“ vs sofort
  4. invoice.payment_failed — Zugangssperre gemäß Policy
  5. Live Keys + neuer Webhook Secret auf DevStudioIT Cloud
  6. Monitoring: Alert bei Webhook 4xx/5xx (Stripe Dashboard)

Grace Period und Zugang bei past_due

Stripe setzt Abo auf past_due nach fehlgeschlagener Zahlung. In Branchly Feld graceUntil — z. B. 7 Tage voller Zugang trotz past_due, danach read-only. Portal lässt Kunden Karte selbst aktualisieren; Webhook invoice.payment_succeeded löscht Grace.

UI-Banner: „Zahlung fehlgeschlagen — Karte aktualisieren“ mit Link zu /api/billing/portal. Kein sofortiger Lockout — senkt Churn und Chargebacks.

FAQ

Unterstützt das Portal EUR und USt-Rechnungen?

Stripe Billing unterstützt EUR; USt-Rechnungen hängen von Tax-Einstellungen und Firmendaten ab. Portal zeigt Rechnungshistorie nach Konfiguration.

Kündigung ausblenden?

Technisch ja (Dashboard), in der EU für B2C oft Self-Service erwartet — Rechtsberatung einholen.

Webhook langsamer als Portal-Redirect?

Nutzer kehrt zurück bevor Branchly aktualisiert — „Konto wird aktualisiert…“ und API-Poll alle 2 s für 30 s.

Ersetzt Customer Portal Stripe Checkout?

Nein. Checkout für erste Zahlung; Portal für bestehendes Abo.

Wo Daten außerhalb Stripe?

Nutzer-Metadaten, Feature Flags, Usage — Branchly. Stripe hält Billing; App mappt priceId → Plan pro / team.

CTA

SaaS mit Self-Service-Billing ohne Monate am Zahlungs-Panel?

Ähnliche Beiträge

Sitemap.xml und RSS-Feed in Next.js App Router — Technisches SEO 2026
3 Min. Lesezeit
RAG-Chatbot — Embeddings, Vektorsuche und Deployment 2026
5 Min. Lesezeit
n8n — Formular-Automatisierung: Webhook aus Next.js → CRM → Slack (2026)
3 Min. Lesezeit

Über den Autor

Wir bauen schnelle Websites, Web/Mobile-Apps, KI-Chatbots und Hosting — mit Fokus auf SEO und Conversion.

Empfohlene Links

Von Theorie zu Produktion — Branchly, Hosting-Stack und Referenzen.

Gefällt euch unser Ansatz? Lasst uns gemeinsam bauen.

Projektkonfiguration starten