Back openDesk Edu for a sovereign, open-source education â every vote counts.
Vote nowSave products you love by clicking the heart icon.
Comprehensive guide to testing Stripe payment integrations â test cards, webhook simulation, checkout flows, edge cases, and CI/CD strategies for bulletproof payment systems.
Als ich anfing, digitale Produkte auf graphwiz.ai und tobias-weiss.org zu verkaufen, hatte ich eine klare Anforderung: Kunden sollten auf einer von Stripe gehosteten Checkout-Seite landen, bezahlen und sofort ihren Download erhalten â und das alles, ohne dass ich ein Warenkorb-UI bauen, mich um die PCI-Compliance kĂŒmmern oder Kreditkartennummern speichern muss.
Stripe Checkout war die offensichtliche Wahl. Es ĂŒbernimmt das Payment-UI, unterstĂŒtzt mehrere WĂ€hrungen (EUR/USD), âPay What You Wantâ-Preise, und die Infrastruktur von Stripe regelt die Compliance. Aber die saubere Integration in eine Next.js-App, die Anbindung von Webhooks fĂŒr die Auslieferung und die Sicherstellung, dass die CSP Stripe.js nicht blockiert, erforderten eine sorgfĂ€ltige Architektur.
Dieser Artikel beschreibt genau, wie ich es umgesetzt habe â die Architektur-Entscheidungen, die Code-Patterns und die Lektionen, die ich dabei gelernt habe.
User â BuyButton â /api/stripe/create-checkout â Stripe Checkout (hosted)
â
Success URL â stripe.com
â
Success Page (verify session)
â
Download + Upsell
â
Webhook (fulfillment + email)
Der Flow ist bewusst zustandslos (stateless) gestaltet. Es werden keine Kundendaten gespeichert, bis die Zahlung erfolgreich war. Die Checkout-Session trĂ€gt alles â Produktname, Slug, Download-URL â in ihren Metadaten.
Ich habe mehrere Plattformen evaluiert, bevor ich dies gebaut habe:
| Ansatz | Vorteile | Nachteile |
|---|---|---|
| Stripe Checkout (meine Wahl) | Volle Kontrolle, keine monatliche GebĂŒhr, eigenes Branding | Store-UI muss gebaut werden, Webhooks-Handling |
| Shopify | SchlĂŒsselfertiger Store, Analytics | Monatliche GebĂŒhren ($39+), Ecosystem-Lock-in |
| Gumroad | Sofortiger Setup, integrierte E-Mails | 8,5 % + $0,30 pro Verkauf, begrenzte Anpassung |
| LemonSqueezy | Steuerhandling, E-Mail-Versand | Drittanbieter-AbhÀngigkeit, weniger Kontrolle |
FĂŒr eine DevOps-orientierte Seite war es die richtige Entscheidung, die Integration selbst zu besitzen. Die Stripe-API ist exzellent, und die monatlichen Ersparnisse bei steigender Skalierung rechtfertigen die Entwicklungszeit.
Das HerzstĂŒck der Integration ist eine einzige API-Route. Sie empfĂ€ngt eine Price-ID und Produkt-Metadaten, erstellt eine Stripe Checkout Session und gibt die Redirect-URL zurĂŒck.
// app/api/stripe/create-checkout/route.ts
import { NextResponse } from "next/server";
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(request: Request) {
const { priceId, productName, productSlug, customerEmail } = await request.json();
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
customer_email: customerEmail,
success_url: `${origin}/store/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/store`,
metadata: { productName, productSlug, downloadUrl },
});
return NextResponse.json({ checkoutURL: session.url });
}
Wichtige Details:
{CHECKOUT_SESSION_ID} ist ein Stripe-Template â Stripe ersetzt dies zum Zeitpunkt des Redirects durch die tatsĂ€chliche Session-ID.Nach der Zahlung leitet Stripe an /store/success?session_id=cs_... weiter. Die Success-Page verifiziert die Session serverseitig:
// app/store/success/page.tsx
const session = await retrieveCheckoutSession(sessionId);
productName = session.metadata?.productName;
if (session.payment_status === "paid") {
// Generate download token
downloadToken = Buffer.from(
JSON.stringify({
session_id: sessionId,
product_slug: productSlug,
timestamp: Date.now(),
}),
).toString("base64");
}
Der Download-Token ist selbstbeschreibend (self-contained) â er benötigt keine Datenbank. Die Download-API-Route dekodiert und verifiziert den Token bei jeder Anfrage gegen die Stripe-API. Das bedeutet, dass Token auch Server-Neustarts ĂŒberstehen (by design zustandslos).
Der Webhook empfĂ€ngt Events von Stripe nach Abschluss des Checkouts. Dies ist der âzuverlĂ€ssige Pfadâ â Stripe versucht die Zustellung erneut, falls der Endpunkt down ist, im Gegensatz zum Redirect der Success-Page, dem der Kunde sofort folgen muss.
// app/api/stripe/webhook/route.ts
const event = constructWebhookEvent(body, signature);
switch (event.type) {
case "checkout.session.completed": {
// Log purchase to purchases.json
// Send confirmation email via local postfix relay
// Generate download token
break;
}
}
Der Webhook muss die Stripe-Signatur mit constructWebhookEvent() verifizieren. Ohne dies könnte jeder, der Ihre Webhook-URL findet, Events fÀlschen und kostenlose Downloads auslösen.
Ein hĂ€ufiger Fehler: Das Webhook-Secret befindet sich in Ihrer .env.production, wird aber nicht automatisch im Stripe-Dashboard konfiguriert. Sie mĂŒssen im Stripe-Dashboard (oder via API) einen Webhook-Endpunkt erstellen, der auf https://yoursite.com/api/stripe/webhook verweist.
curl https://api.stripe.com/v1/webhook_endpoints \
-u "sk_live_..." \
-d "url=https://yoursite.com/api/stripe/webhook" \
-d "enabled_events[]=checkout.session.completed" \
-d "enabled_events[]=checkout.session.expired"
Das von diesem API-Aufruf zurĂŒckgegebene whsec_... Secret muss mit Ihrem STRIPE_WEBHOOK_SECRET ĂŒbereinstimmen. Sie bilden ein passendes Paar â wenn Sie eines regenerieren, wird das andere ungĂŒltig.
Wenn der Webhook ausgelöst wird, versende ich eine BestĂ€tigungs-E-Mail ĂŒber ein lokales Postfix-Relay, das auf dem Server lĂ€uft. Dadurch werden Drittanbieter-E-Mail-Dienste vermieden:
const transporter = nodemailer.createTransport({
host: "127.0.0.1",
port: 2525,
secure: false,
ignoreTLS: true,
});
await transporter.sendMail({
from: '"Store" <noreply@tobias-weiss.org>',
replyTo: customerEmail,
to: process.env.CONTACT_EMAIL,
subject: "New Purchase: " + productName,
text: "..." + downloadLink + "...",
});
Die E-Mail enthĂ€lt den Download-Link als Backup â selbst wenn der Kunde den Browser nach der Zahlung schlieĂt, erhĂ€lt er den Link in seinem Posteingang.
Stripe.js erfordert spezifische CSP-Direktiven. Hier ist die restriktivste Konfiguration, die funktioniert:
script-src 'self' 'unsafe-inline' https://js.stripe.com;
connect-src 'self' https://api.stripe.com;
frame-src https://js.stripe.com;
img-src https://*.stripe.com;
Hinweis: 'unsafe-inline' ist fĂŒr Next.js Runtime-Chunks und Stripe.js erforderlich. Sie können dies nicht entfernen, ohne eines der Frameworks zu beeintrĂ€chtigen.
Stripe begrenzt Metadatenwerte auf jeweils 500 Zeichen. Wenn Ihr Produktname lang ist, kĂŒrzen Sie ihn. Das downloadUrl muss kurz sein.
Alle Preis-IDs im Test-Modus (price_1Test...) sind vollstĂ€ndig getrennt vom Live-Modus (price_1Live...). Sie mĂŒssen in beiden Umgebungen separate Preise erstellen. Das Gleiche gilt fĂŒr Webhook-Endpunkte â Stripe hat separate Webhook-Konfigurationen fĂŒr Test und Live.
{CHECKOUT_SESSION_ID} TemplateDiese Template-Variable funktioniert nur in der success_url und cancel_url einer Checkout-Session. Es handelt sich nicht um eine allgemeine Stripe-Funktion. Ich hatte sie anfangs weggelassen â die Erfolgsseite konnte den Kauf nicht verifizieren, da sie keine Session-ID hatte.
Falls Sie jemals von Checkout zu Stripe Elements (eingebettetes Zahlungsformular) wechseln, benötigen Sie zusĂ€tzliche CSP-Direktiven fĂŒr https://m.stripe.com und https://js.stripe.com. Checkout verarbeitet das Zahlungsformular auf der Domain von Stripe, weshalb dies einfacher ist.
Stripe versucht die Zustellung fehlgeschlagener Webhooks bis zu 3 Tage lang zu wiederholen. Ihr Handler muss idempotent sein â die zweifache Verarbeitung desselben Events sollte keine zusĂ€tzlichen Seiteneffekte haben. Meine Implementierung prĂŒft auf bestehende EintrĂ€ge in purchases.json, bevor Duplikate hinzugefĂŒgt werden.
Das beschriebene Stripe-Integrationsmuster ist wiederverwendbar. Hier sind Produkte, die ich auf dieser Basis entwickeln möchte:
Jedes dieser Produkte wĂŒrde natĂŒrlich neben den bestehenden DevOps-Stacks in den Store passen.
Stripe Checkout + Next.js ist eine leistungsstarke Kombination fĂŒr den Verkauf digitaler Produkte. Die Architektur ist simpel â API-Route â Stripe Checkout â Erfolgsseite + Webhook â und skaliert von 1 ⏠Community-Supporter-Tokens bis hin zu 79 ⏠Produktions-Guides.
Der SchlĂŒssel zum Erfolg in der Produktion:
.env.production aufbewahrenDer vollstĂ€ndige Code fĂŒr beide Stores ist Open-Source unter github.com/tobias-weiss-ai-xr verfĂŒgbar.