Stripe-Checkout-Integration: Ablauf und Live-Fallstricke

Eine Stripe-Checkout-Integration besteht aus zwei Teilen, die beide sitzen müssen: Dein Server legt eine Checkout Session an und schickt den Kunden auf die von Stripe gebaute Zahlungsseite, und ein Webhook-Endpunkt schaltet nach der Zahlung frei. Der zweite Teil entscheidet, ob die Integration im Live-Betrieb hält. Wer die Freischaltung an die Rückkehr auf die Erfolgsseite hängt, verliert Käufe, sobald ein Kunde den Tab schließt.

Ich habe Stripe Checkout in mehreren eigenen Produkten eingebaut, darunter eine Einmalzahlung für einen Lifetime-Zugang und Abos mit Kundenportal. Die Beispiele unten stammen aus diesem Code, nicht aus der Dokumentation.

Hosted, embedded oder eigenes Formular

Stripe Checkout ist eine vorgefertigte Zahlungsseite. Laut Stripe-Support lässt sie sich entweder direkt in die eigene Website einbetten oder auf einer von Stripe gehosteten Seite ausliefern; beide Varianten unterstützen dieselben Zahlungsmethoden und fast dieselben Funktionen (Stripe Support: Embedded vs. hosted Checkout). Der dort genannte Unterschied: Nur die eingebettete Variante kann einen eingebauten Erfolgsstatus direkt in Checkout anzeigen.

VarianteWann sie passtAufwand im Frontend
Hosted CheckoutEinmalzahlung oder Abo, Kunde darf die Seite kurz verlassenEin Button und ein Redirect
Embedded CheckoutKunde soll auf deiner Domain bleibenStripe.js-Komponente einbinden, Rücksprung-Seite bauen
Eigenes Formular (Elements)Bezahlung ist Teil eines mehrstufigen eigenen AblaufsFormular, Validierung, Fehlerzustände selbst bauen

Meine Entscheidungsregel: Hosted Checkout, solange niemand einen konkreten Grund dagegen nennt. Die Seite lässt sich in Erscheinungsbild und Branding anpassen, mit eigener Domain betreiben und um Produktbilder ergänzen (Stripe Docs: Checkout anpassen). Das deckt die meisten Wünsche an "es soll nach uns aussehen" ab, ohne ein Zahlungsformular selbst zu pflegen.

Der Ablauf in einer Next.js-App

Der Ablauf hat drei Stationen. Jede gehört auf den Server, nicht in den Browser.

1. Checkout Session serverseitig anlegen

Ein Route Handler (zum Beispiel app/api/checkout/route.ts) prüft zuerst den Login, dann den bestehenden Zugriff, und erst danach ruft er stripe.checkout.sessions.create auf. Die Session bekommt mode (payment für Einmalzahlung, subscription für Abos), die price-ID aus der Umgebung, eine success_url und eine cancel_url. Zurück an den Browser geht nur session.url.

Zwei Felder lohnen sich immer dann, wenn der Käufer ein eingeloggter Nutzer ist: client_reference_id mit der User-ID und metadata mit dem, was freigeschaltet werden soll. Der Webhook findet den Nutzer später darüber, ohne die E-Mail-Adresse abgleichen zu müssen.

Preise gehören nicht in den Client. Wer den Betrag aus dem Browser übernimmt, lässt Kunden ihren Preis selbst wählen.

2. Weiterleitung und Rückkehr

Der Browser leitet auf session.url weiter. Nach der Zahlung schickt Stripe den Kunden auf die success_url. Diese Seite zeigt eine Bestätigung und sonst nichts. Sie setzt keinen Zugriff, schreibt nichts in die Datenbank und versendet keine Mail.

3. Freischaltung per Webhook

Der Webhook-Endpunkt liest den Request als Rohtext (request.text(), nicht request.json()), prüft die Signatur mit stripe.webhooks.constructEvent und reagiert dann auf das passende Event. In meiner Einmalzahlung sind das checkout.session.completed und checkout.session.async_payment_succeeded, und freigeschaltet wird nur, wenn payment_status den Wert paid hat. Damit fallen Zahlungsarten, die erst später bestätigt werden, nicht durch.

Zum lokalen Testen leitet die Stripe CLI Events an den eigenen Rechner weiter; das offizielle Beispielprojekt nutzt dafür localhost:4242/webhook und enthält Server-Implementierungen in acht Sprachen, darunter Node, PHP, Python und Go (stripe-samples/checkout-one-time-payments auf GitHub).

Was im Live-Betrieb schiefgeht

Die folgenden Fehler habe ich entweder selbst gemacht oder in Code gefunden, den ich übernommen habe. Keiner fällt im Testmodus auf.

Ein Leerzeichen im Webhook-Secret. Das deployte STRIPE_WEBHOOK_SECRET hatte einen Zeilenumbruch am Ende. constructEvent lehnte daraufhin jede Signatur ab, der Endpunkt lief mit 100 Prozent Fehlerquote, und Käufe wurden bezahlt, aber nicht freigeschaltet. Die Korrektur ist eine Zeile: Secrets beim Einlesen mit .trim() säubern. Erkennbar ist der Fehler im Stripe-Dashboard an fehlgeschlagenen Webhook-Zustellungen, lange bevor sich Kunden melden.

Doppelte Events. Rechne damit, dass dasselbe Event mehr als einmal ankommt, etwa wenn dein Endpunkt beim ersten Versuch zu langsam antwortet. Wer bei jedem Event eine Bestätigungsmail schickt, schickt sie mehrfach. Die Freischaltung muss idempotent sein: Erst prüfen, ob der Zugriff schon gesetzt ist, und nur beim ersten Mal weitere Schritte auslösen. Ich nutze dafür die Session-ID als Schlüssel.

Live-Key mit localhost-URL. Wer lokal versehentlich mit Live-Schlüsseln arbeitet, erzeugt echte Zahlungen mit Rücksprung auf localhost. Mein Checkout-Endpunkt bricht deshalb ab, wenn der Secret Key mit sk_live_ beginnt und die App-URL auf localhost zeigt.

Freischaltung auf der Erfolgsseite. Siehe oben. Erkennbar daran, dass in der Datenbank Käufe fehlen, die im Stripe-Dashboard als bezahlt stehen.

Aufwand: Einmalzahlung oder Abo

Die Zahlen sind ein Rechenbeispiel für eine bestehende Next.js-App mit Login, keine Preisangabe.

BausteinEinmalzahlung (Std.)Abo mit Kundenportal (Std.)
Session-Endpunkt und Button2–33–5
Webhook mit Signaturprüfung und Idempotenz4–68–12
Kündigung, Planwechsel, Zahlungsausfall–6–10
Kundenportal einbinden–2–4
Tests mit Stripe CLI, Live-Abnahme3–64–6
Dokumentation und Übergabe1–31–3
Summe10–1824–40

Der größte Posten beim Abo ist nicht der Kauf, sondern alles danach: Kündigungen, gescheiterte Abbuchungen und Planwechsel erzeugen jeweils eigene Events, die deine Datenbank nachziehen muss. Wenn ein Projekt KI-Funktionen mit nutzungsabhängigen Kosten enthält, gehört zusätzlich die Frage dazu, wie Abo-Stufen und API-Kosten zusammenpassen; die Kostenfaktoren dafür stehen unter KI-Integration in Web-Apps: Kostenfaktoren und Budgetplanung. Wie ich externe APIs in Next.js generell kapsle und cache, zeigt die Wetter-API-Integration in Next.js.

Rechtliche Punkte am Checkout, etwa Widerrufsbelehrung, Preisangaben oder Rechnungsstellung, hängen am Geschäftsmodell und gehören vor dem Go-live auf die Liste für Anwalt oder Steuerberater. Technisch einplanen lassen sie sich trotzdem früh, zum Beispiel als zusätzliches Häkchen vor dem Weiterleiten auf Stripe.

Häufige Fragen

Was ist die Stripe Checkout API?

Gemeint ist meist der Endpunkt zum Anlegen einer Checkout Session. Dein Server übergibt Produkt, Preis, Modus und Rücksprung-URLs und bekommt eine URL zur Zahlungsseite zurück. Alles Weitere, also Kartenformular, Validierung und 3D Secure, übernimmt die Stripe-Seite.

Wie integriere ich Stripe Checkout in Next.js?

Mit einem Route Handler, der die Session anlegt und session.url zurückgibt, und einem zweiten Route Handler für den Webhook. Der Webhook liest den Rohtext des Requests, prüft die Signatur und schaltet erst dann frei. Die Erfolgsseite zeigt nur eine Bestätigung.

Gibt es ein Beispiel für Embedded Checkout?

Stripe pflegt Beispielprojekte auf GitHub, etwa für Einmalzahlungen mit hosted Checkout in acht Server-Sprachen. Für die eingebettete Variante ändert sich vor allem das Frontend: Statt eines Redirects bindest du die Checkout-Komponente von Stripe.js ein und baust eine Rücksprung-Seite. Webhook und Freischaltung bleiben gleich.

Kann ich die Stripe-Checkout-Seite an mein Design anpassen?

Teilweise. Erscheinungsbild und Branding, eigene Domain und Produktbilder lassen sich laut Stripe-Dokumentation anpassen. Wer die Zahlungsmaske selbst gestalten will, landet bei Elements und übernimmt damit auch Validierung und Fehlerzustände.

Passende Umsetzung aus meinem Portfolio

Konkretes Projekt im Kopf?

Eine kurze Mail mit dem Ziel genügt. Antwort innerhalb von 48 Stunden.

Projekt anfragen