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:
- Nutzer meldet sich an (NextAuth, Clerk, eigenes JWT)
- In Branchly (branchly.cloud) liegen
users.stripeCustomerIdundsubscriptions.status - Klick „Abo verwalten“ → POST
/api/billing/portal - Server:
stripe.billingPortal.sessions.create({ customer, return_url }) - Redirect zur Stripe-Session → Kunde bearbeitet → Rückkehr zu
return_url - Webhook
customer.subscription.updatedaktualisiert 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
- Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhooks/stripe - Plan-Upgrade im Portal testen → Webhook und UI prüfen
- Kündigung „zum Periodenende“ vs sofort
invoice.payment_failed— Zugangssperre gemäß Policy- Live Keys + neuer Webhook Secret auf DevStudioIT Cloud
- 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?
- Stripe + Next.js besprechen — Checkout, Portal, Webhooks, Branchly
- Webanwendungen — Abos, Hosting DevStudioIT Cloud
Ü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.
