Stripe-Zahlungen mit Claude Code einbauen
Verbinde Stripe Checkout, Webhooks und das Customer Portal in einer Next.js-App mit Claude Code. Vom ersten Prompt bis zur Live-Zahlung in einer Session.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Problem: Eine Stripe-Integration hat 7 bewegliche Teile: Checkout, Webhooks, das Customer Portal, Environment-Variablen, Test-Keys, Live-Keys und die CLI. Die meisten Tutorials decken drei davon ab und lassen dich den Rest um Mitternacht debuggen.
Quick Win: Füg den Stripe-MCP-Server mit einem einzigen Kommando zu Claude Code hinzu, damit Claude beim Bauen in Echtzeit Kunden, Preise und Abonnements nachschlagen kann:
claude mcp add --transport http stripe https://mcp.stripe.com/Diese eine Zeile gibt Claude direkten Zugriff auf deinen Stripe-Account. Es kann Live-Daten lesen, prüfen, ob deine Produkte existieren, und Config-Fehler abfangen, bevor sie deinen Webhook-Handler erreichen.
Was du vorher brauchst
Drei Dinge müssen stehen.
Ein Stripe-Account mit mindestens einem angelegten Produkt und Preis. Du kannst Claude die über den MCP-Server anlegen lassen, ohne das Dashboard anzufassen. Sobald die MCP verbunden ist, reicht ein Prompt wie „create a product called Pro Plan with a $29/month recurring price".
Claude Code mit verbundener Stripe-MCP (Kommando oben). Die Alternative ist der Stripe-Best-Practices-Skill, der Stripe-Dokumentation in deine Session injiziert:
npx skills add https://docs.stripe.com --yesDer MCP-Server ist für aktive Entwicklung die bessere Wahl. Claude kann ihn mitten in der Session aufrufen, um deine Preise aufzulisten, einen Kunden per E-Mail nachzuschlagen, den aktuellen Status eines Abonnements abzurufen oder zu prüfen, ob dein Webhook-Secret zu dem passt, was das Dashboard zeigt. Der Skill gibt dir nur Dokumentationskontext. Er fragt deinen Account nicht ab.
Die beiden schließen sich nicht aus. Manche Teams fahren beides: MCP für Live-Abfragen, Skill für Best-Practice-Anleitung. Die MCP mit einem eingeschränkten API-Key (rk_test_...) ist im Testmodus das sicherere Setup, weil eingeschränkte Keys begrenzen, welche Stripe-Objekte Claude anfassen kann.
Vier Environment-Variablen in deiner .env.local-Datei:
STRIPE_SECRET_KEY=sk_test_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_URL=http://localhost:3000STRIPE_SECRET_KEY berührt nie den Client. NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY darf offengelegt werden. STRIPE_WEBHOOK_SECRET kommt aus der Stripe CLI, wenn du das lokale Event-Forwarding startest (im Test-Abschnitt behandelt). NEXT_PUBLIC_URL speist deine success_url- und return_url-Parameter.
Checkout Sessions vs. Payment Intents
Zwei Stripe-APIs können eine Zahlung entgegennehmen. Die falsche zu wählen kostet dich Wochen Arbeit.
Checkout Sessions (stripe.checkout.sessions.create()) leiten den User auf eine von Stripe gehostete Seite oder ein eingebettetes Formular auf deiner Domain. Stripe kümmert sich um Karteneingabe, 3D Secure, Apple Pay, Währungsanzeige und Steuer. Für die meisten SaaS-Produkte ist das die richtige Wahl. Einmalkäufe und Abonnements laufen beide darüber.
Payment Intents (stripe.paymentIntents.create()) sind tiefergehend. Du baust das Formular selbst mit Stripe Elements, sammelst Kartendaten und rufst die API auf. Mehr Kontrolle, mehr Code, mehr Wartung. Nutz das nur, wenn du eine voll eigene Checkout-UI brauchst, die eine gehostete Seite nicht liefern kann.
Das 2026 bevorzugte Muster für Checkout Sessions ist Embedded Checkout: ui_mode: 'embedded' hält den User auf deiner Domain, nutzt die Komponenten EmbeddedCheckoutProvider und EmbeddedCheckout aus @stripe/react-stripe-js und kehrt zu einer return_url zurück, die du setzt. Hosted Checkout (Redirect zu stripe.com) funktioniert weiterhin, aber Embedded ist das, wozu Stripe neue Integrationen steuert.
Den Checkout-Flow bauen
Bitte Claude, die Route zu scaffolden, und prüf dann den mode-Parameter, bevor du auslieferst.
Ein nützlicher Prompt für Claude hier ist: „Build me a Stripe Checkout Sessions route in app/api/checkout/route.ts. Use mode: payment for one-time purchases, return the session URL as JSON, and read all URLs from environment variables."
Für einen Einmalkauf sieht die Route so aus:
// app/api/checkout/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const { priceId } = await request.json()
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}Für ein Abonnement änderst du genau eine Sache. Setz mode: 'subscription' statt mode: 'payment'. Deine priceId muss auf einen wiederkehrenden Preis zeigen (nicht auf einen Einmalpreis) in Stripe. Alles andere in der Route bleibt identisch.
Um auf Embedded Checkout umzustellen, füg ui_mode: 'embedded' hinzu und ersetz success_url durch return_url. Die Antwort gibt session.client_secret zurück statt session.url, das der EmbeddedCheckoutProvider im Frontend nutzt.
Webhooks: Der Teil, den alle falsch machen
Webhooks sind, wo Stripe zu deiner App zurückspricht. Jede Bereitstellungsentscheidung (Zugriff gewähren, Zugriff entziehen, eine Quittung senden) sollte im Webhook-Handler leben, nicht im Checkout-Success-Redirect. User schließen Tabs und drücken den Zurück-Button. Der Redirect ist unzuverlässig. Der Webhook feuert immer.
Das kritische Problem im Next.js App Router ist das Parsen des Raw Body. Stripes Signaturprüfung (stripe.webhooks.constructEvent) braucht den rohen Request-Body als String. Der App Router parst den Body, bevor dein Handler ihn sieht. Wenn du request.json() aufrufst, wird der Stream verbraucht, und die Prüfung scheitert jedes Mal.
Gib Claude die exakte Vorgabe: „Build a Stripe webhook handler at app/api/stripe-webhook/route.ts. It must use request.text() for the raw body, not request.json(), and verify the signature before processing any events."
Nutz stattdessen request.text():
// app/api/stripe-webhook/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const rawBody = await request.text()
const sig = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return NextResponse.json(
{ error: 'Webhook signature verification failed' },
{ status: 400 }
)
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session
// Provision access, send welcome email
await grantAccess(session.customer as string)
break
}
case 'customer.subscription.updated': {
const sub = event.data.object as Stripe.Subscription
// Update tier in DB
await updateSubscription(sub.customer as string, sub.status)
break
}
case 'customer.subscription.deleted': {
const sub = event.data.object as Stripe.Subscription
// Revoke access
await revokeAccess(sub.customer as string)
break
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice
// Send dunning email, flag in DB
await handleFailedPayment(invoice.customer as string)
break
}
}
return NextResponse.json({ received: true })
}Die vier Events, die du mindestens behandeln solltest, stehen in der Tabelle unten. Den Rest kannst du hinzufügen, wenn deine Billing-Logik wächst.
| Event | Wann es feuert | Aktion |
|---|---|---|
checkout.session.completed | Zahlung oder Abo gestartet | Zugriff bereitstellen, Willkommens-E-Mail senden |
customer.subscription.updated | Plan-Wechsel, Verlängerung, Trial-Ende | DB-Tier aktualisieren |
customer.subscription.deleted | Kündigung | Zugriff entziehen |
invoice.payment_failed | Wiederkehrende Belastung fehlgeschlagen | Dunning-E-Mail senden, in DB markieren |
Noch eine Sache: Diese Route muss von jeder Next.js-Middleware ausgenommen werden, die den Body liest, und sie darf nicht in die Auth-Checks deiner App gehüllt sein. Stripe postet als unauthentifizierter Request dorthin. Schütz sie nur über die Signaturprüfung.
Customer Portal einrichten
Das Customer Portal lässt User ihr eigenes Abonnement verwalten: kündigen, Zahlungsinfos aktualisieren, Rechnungen ansehen. Ein Schritt bringt die meisten Entwickler aus dem Tritt.
Du musst das Portal im Stripe-Dashboard konfigurieren, bevor du irgendwelche Portal-Sessions erstellst. Ohne gespeicherte Konfiguration gibt jeder billingPortal.sessions.create()-Aufruf einen Fehler zurück. Die Konfigurations-URL für den Testmodus ist https://dashboard.stripe.com/test/settings/billing/portal. Der Live-Modus hat seine eigene, separate URL unter https://dashboard.stripe.com/settings/billing/portal. Beide brauchen vor dem Deploy eine Konfiguration.
Der Konfigurationsschritt ist, wo du entscheidest, welche Aktionen User vornehmen dürfen: ihr Abonnement kündigen, ihre Zahlungsmethode aktualisieren, Pläne wechseln, Rechnungen herunterladen. Setz diese Optionen im Dashboard, speichere die Konfiguration, und dann funktionieren deine API-Aufrufe.
Einmal konfiguriert, ist die Route zum Erstellen einer Session kurz:
// app/api/portal/route.ts
import { NextResponse } from 'next/server'
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-11-20.acacia',
})
export async function POST(request: Request) {
const { customerId } = await request.json()
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_URL}/account`,
})
return NextResponse.json({ url: session.url })
}Dein Frontend leitet auf session.url weiter. Der User sieht seine Stripe-Billing-Seite, nimmt Änderungen vor und kehrt zu /account zurück, wenn er fertig ist. Die Änderungen feuern als customer.subscription.updated- oder customer.subscription.deleted-Events, die dein Webhook-Handler bereits abdeckt.
Hinweis: Test- und Live-Modus-Portal-Konfigurationen sind komplett getrennt. Das Portal im Testmodus zu konfigurieren richtet es nicht für den Live-Modus ein. Geh dieselben Konfigurationsschritte in beiden Umgebungen durch, bevor du live gehst.
Den kompletten Flow testen
Die Stripe CLI leitet Live-Events von Stripe an deinen lokalen Server weiter. Führ das in einem separaten Terminal aus, bevor du irgendwelche Zahlungen testest:
stripe listen --forward-to localhost:3000/api/stripe-webhookDie CLI gibt beim Start ein Webhook-Signing-Secret aus. Dieser Wert kommt in STRIPE_WEBHOOK_SECRET in deiner .env.local. Er ändert sich jedes Mal, wenn du den Listener neu startest — hardcode ihn also nicht.
Um ein bestimmtes Event auszulösen, ohne dich durch ein Zahlungsformular zu klicken, nutz stripe trigger:
stripe trigger checkout.session.completed
stripe trigger customer.subscription.deleted
stripe trigger invoice.payment_failedTestkarten für verschiedene Szenarien:
| Szenario | Kartennummer |
|---|---|
| Erfolgreiche Zahlung | 4242 4242 4242 4242 |
| Abgelehnt | 4000 0000 0000 0002 |
| 3D Secure erforderlich | 4000 0025 0000 3155 |
| Unzureichende Deckung | 4000 0000 0000 9995 |
Nimm ein beliebiges zukünftiges Ablaufdatum, einen beliebigen dreistelligen CVC und eine beliebige PLZ. Die Kartennummer ist der einzige Wert, der das Ergebnis ändert.
Die empfohlene Test-Sequenz ist: Starte den CLI-Listener, öffne deine Checkout-Seite, schließ eine Zahlung mit Karte 4242 4242 4242 4242 ab, beobachte, wie das checkout.session.completed-Event in deinem Terminal eintrifft, und prüf, ob dein Handler lief (check die DB oder die Logs). Dann löse stripe trigger invoice.payment_failed aus, um zu bestätigen, dass dein Dunning-Pfad funktioniert, ohne auf eine echte fehlgeschlagene Belastung zu warten. Die 3D-Secure-Karte (4000 0025 0000 3155) deck zuletzt ab. Stripe leitet durch einen Authentifizierungsschritt, den dein Frontend behandeln muss. Wenn der Redirect still scheitert, wird die Session nie abgeschlossen und kein Webhook feuert.
Going-Live-Checkliste
Der Wechsel von Test- zu Live-Modus sind vier Schritte, nicht ein Key-Tausch.
Erstens: Ersetz deine Test-Keys (sk_test_, pk_test_) durch Live-Keys (sk_live_, pk_live_) in deinen Production-Environment-Variablen.
Zweitens: Konfigurier das Customer Portal im Live-Modus unter https://dashboard.stripe.com/settings/billing/portal. Test- und Live-Modus haben komplett getrennte Portal-Konfigurationen. Was du im Testmodus setzt, wird nicht übernommen.
Drittens: Setz deinen Webhook-Endpoint im Stripe-Dashboard auf deine Production-URL. Geh zu Developers > Webhooks > Add endpoint und zeig auf https://yourdomain.com/api/stripe-webhook. Kopier das Live-Webhook-Signing-Secret und setz es als STRIPE_WEBHOOK_SECRET in Production.
Viertens: Bestätige deine Preis-IDs. Test-Modus-Preise (price_test_...) existieren im Live-Modus nicht. Alle hartcodierten Preis-IDs müssen auf Live-Preis-IDs aktualisiert oder aus Environment-Variablen gelesen werden.
Führ eine echte Zahlung mit einer echten Karte durch (und erstatte sie dann), bevor du die Integration für fertig erklärst. Der CLI-Trigger ist gut zum Testen von Handlern, aber nichts bestätigt den kompletten Pfad so wie eine tatsächliche Belastung.
Was Build This Now vorgebaut ausliefert
Diesen Flow zu schreiben und zu debuggen kostet die meisten Entwickler einen ganzen Tag. Stripes Checkout-Edge-Function, der Webhook-Handler, die Customer-Portal-Route und alle vier Environment-Variablen sind in Build This Now vom ersten Tag an vorverdrahtet. Die Integration deployt auf Supabase Edge Functions, getestet und funktionierend, bevor du dein erstes eigenes Feature schreibst.
Stripe ist nicht der schwere Teil. Jedes Stück in der richtigen Reihenfolge verbunden zu bekommen, schon. Hast du es einmal gemacht, weißt du, wo die Lücken sind.
Hören Sie auf zu konfigurieren. Fangen Sie an zu bauen.
SaaS-Builder-Vorlagen mit KI-Orchestrierung.
Claude Code Auto-Modus
Ein zweites Sonnet-Modell prüft jeden Claude Code-Tool-Aufruf, bevor er ausgeführt wird. Was der Auto-Modus blockiert, was er erlaubt, und die Erlaubnisregeln, die er in deine Einstellungen schreibt.
Feedback-Loops
Gib Claude Code einen einzigen Prompt, der Code schreibt, deinen Test- oder Dev-Befehl ausführt, die Ausgabe liest, alles Kaputte behebt und schleift, bis die Suite grün ist.