Die meisten Stripe-Integrationen scheitern an derselben Stelle: Sie betrachten die Browser-Weiterleitung als Zahlungsnachweis. Das ist es nicht. Die einzige zuverlässige Quelle der Wahrheit ist der Webhook, den Stripe an Ihren Server sendet. Dieser Leitfaden baut eine korrekte Integration in Next.js mit dem App Router auf – Checkout, Webhooks mit Signaturüberprüfung, idempotente Erfüllung und Abonnements.
📋 Table of Contents
- Architektur zuerst
- Schritt 1: Installieren und konfigurieren Sie
- Schritt 2: Erstellen Sie die Checkout-Sitzung
- Schritt 3: Vom Client umleiten
- Schritt 4: Der Webhook – Wo Korrektheit lebt
- Schritt 5: Webhooks lokal testen
- Schritt 6: Die Erfolgsseite bestätigt, dass
- Schritt 7: Lassen Sie Kunden ihre eigenen Abonnements verwalten
- Going-Live-Checkliste
- verfügt Häufige Fehler
- Fazit
Architektur zuerst
Das Verständnis des Ablaufs verhindert die beiden Fehler, die die meisten Integrationen zerstören.
- Ihr Server erstellt eine Checkout-Sitzung und gibt deren URL zurück.
- Der Browser leitet zu Stripe weiter. Der Benutzer zahlt auf der Seite von Stripe – Kartendaten berühren niemals Ihren Server.
- Stripe leitet den Browser zurück zu Ihrer Erfolgs-URL.
- Separatsendet Stripe einen Webhook an Ihren Server, der die Zahlung bestätigt.
- Ihr Webhook-Handler gewährt Zugriff. Nicht die Erfolgsseite.
Schritt 5 ist das ganze Spiel. Der Benutzer kann die Registerkarte vor der Weiterleitung schließen, die Verbindung verlieren oder einfach die Erfolgs-URL bearbeiten und sie direkt besuchen. Gewähren Sie Zugriff nur auf den Webhook.
Schritt 1: Installieren und konfigurieren Sie
npm install stripe @stripe/stripe-js
Speichern Sie Schlüssel in Umgebungsvariablen. Der geheime Schlüssel darf niemals den Client erreichen, daher erhält er keinNEXT_PUBLIC_ Präfix.
# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Erstellen Sie einen einzelnen gemeinsam genutzten Stripe-Client, damit Sie nicht einen pro Anfrage erstellen.
// lib/stripe.ts
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is not set');
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2026-06-30',
typescript: true,
});
AnheftenapiVersion Angelegenheiten. Ohne sie erben Sie die Version, die Ihr Konto standardmäßig verwendet, und das kann sich unter Ihnen ändern.
Schritt 2: Erstellen Sie die Checkout-Sitzung
Akzeptieren Sie niemals einen Preis vom Kunden. Senden Sie eine Produktkennung und schauen Sie auf dem Server nach dem Preis, oder der Benutzer bearbeitet einfach die Anfrage und zahlt einen Cent.
// app/api/checkout/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';
const PRICES: Record<string, string> = {
pro_monthly: 'price_1AbCdEfGhIjKlMnO',
pro_yearly: 'price_1XyZaBcDeFgHiJkL',
};
export async function POST(req: Request) {
const user = await getCurrentUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { plan } = await req.json();
const priceId = PRICES[plan];
if (!priceId) {
return NextResponse.json({ error: 'Unknown plan' }, { status: 400 });
}
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/welcome?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/pricing`,
customer_email: user.email,
// client_reference_id survives the round trip and arrives in the webhook.
client_reference_id: user.id,
metadata: { userId: user.id, plan },
});
return NextResponse.json({ url: session.url });
}
client_reference_id undmetadata So erfahren Sie, welcher Benutzer bezahlt hat, wenn der Webhook eintrifft. Wenn Sie sie weglassen, werden Zahlungen per E-Mail mit Konten abgeglichen, was unterbrochen wird, sobald jemand mit einer anderen Adresse bezahlt.
Schritt 3: Vom Client umleiten
'use client';
export function UpgradeButton({ plan }: { plan: string }) {
async function handleClick() {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ plan }),
});
if (!res.ok) {
alert('Could not start checkout. Please try again.');
return;
}
const { url } = await res.json();
window.location.href = url;
}
return <button onClick={handleClick}>Upgrade</button>;
}
Schritt 4: Der Webhook – Wo Korrektheit lebt
Zwei Dinge unterbrechen Webhooks in Next.js. Zuerst müssen Sie die Signatur anhand desüberprüfen roh Anfragetext; Wenn irgendetwas es zuerst in JSON analysiert, schlägt die Überprüfung fehl. Zweitens müssen Sie mit doppelten Übermittlungen umgehen, da Stripe wiederholt versucht, dasselbe Ereignis mehr als einmal zu übermitteln.
// app/api/webhooks/stripe/route.ts
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { grantAccess, revokeAccess, hasProcessed, markProcessed } from '@/lib/billing';
export async function POST(req: Request) {
// req.text() gives the raw body, which signature verification requires.
const body = await req.text();
const signature = (await headers()).get('stripe-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
}
let event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Signature verification failed:', err);
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
// Stripe retries on any non-2xx, so the same event can arrive repeatedly.
if (await hasProcessed(event.id)) {
return NextResponse.json({ received: true });
}
try {
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
const userId = session.metadata?.userId ?? session.client_reference_id;
if (userId) {
await grantAccess(userId, {
customerId: session.customer as string,
subscriptionId: session.subscription as string,
});
}
break;
}
case 'customer.subscription.deleted': {
await revokeAccess(event.data.object.customer as string);
break;
}
case 'invoice.payment_failed': {
// Notify the user; do not revoke immediately — cards fail temporarily.
break;
}
}
await markProcessed(event.id);
return NextResponse.json({ received: true });
} catch (err) {
console.error('Webhook handler failed:', err);
// Return 500 so Stripe retries rather than dropping the event.
return NextResponse.json({ error: 'Handler failed' }, { status: 500 });
}
}
hasProcessed undmarkProcessed sollte mit einer eindeutigen Einschränkung in eine Tabelle schreiben, die auf der Stripe-Ereignis-ID basiert. Diese einzige Einschränkung macht die Erfüllung idempotent – ohne sie wird das Konto bei einem erneuten Versuch doppelt gutgeschrieben.
Schritt 5: Webhooks lokal testen
Stripe kannlocalhostnicht erreichen , also leiten Sie Ereignisse mit der CLI weiter.
stripe login
stripe listen --forward-to localhost:3000/api/webhooks/stripe
# In another terminal, fire a test event:
stripe trigger checkout.session.completed
stripe listen Gibt ein Webhook-Signaturgeheimnis aus. Verwenden Sie diesen Wert fürSTRIPE_WEBHOOK_SECRET in der Entwicklung – es unterscheidet sich von dem im Dashboard.
Schritt 6: Die Erfolgsseite bestätigt, dass
nicht gewährt wird Auf der Erfolgsseite sollte der Status angezeigt werden, den Ihr Webhook bereits geschrieben hat. Wenn der Webhook noch nicht gelandet ist, zeigen Sie einen ausstehenden Status an, anstatt etwas zu gewähren.
// app/welcome/page.tsx
import { getCurrentUser } from '@/lib/auth';
import { getSubscription } from '@/lib/billing';
export default async function WelcomePage() {
const user = await getCurrentUser();
const subscription = await getSubscription(user.id);
if (!subscription?.active) {
return (
<p>Payment received. Your account is being activated — this usually takes a few seconds.</p>
);
}
return <h1>Welcome to Pro</h1>;
}
Schritt 7: Lassen Sie Kunden ihre eigenen Abonnements verwalten
Das Abrechnungsportal verarbeitet Planänderungen, Stornierungen, Rechnungen und Kartenaktualisierungen. Der Aufbau dieser Flows selbst ist wochenlange Arbeit, die Sie nicht leisten müssen.
// app/api/portal/route.ts
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { getCurrentUser } from '@/lib/auth';
import { getCustomerId } from '@/lib/billing';
export async function POST() {
const user = await getCurrentUser();
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const customerId = await getCustomerId(user.id);
if (!customerId) return NextResponse.json({ error: 'No subscription' }, { status: 400 });
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${process.env.NEXT_PUBLIC_BASE_URL}/account`,
});
return NextResponse.json({ url: session.url });
}
Going-Live-Checkliste
- Tauschen Sie Testschlüssel gegen Live-Schlüssel aus und erstellen Sie eingetrennt Webhook-Endpunkt im Live-Modus mit eigenem Signaturgeheimnis
- Stellen Sie sicher, dass Ihre Webhook-URL öffentlich erreichbar ist und schnell 2xx zurückgibt – führen Sie asynchrone langsame Arbeiten aus
- Aktivieren Sie Stripe Tax, wenn Sie länderübergreifend verkaufen
- Aktivieren Sie Radarregeln zum Schutz vor Betrug
- Testen Sie den gesamten Ablauf mit einer echten Karte und erstatten Sie dann den Betrag
- Stellen Sie sicher, dass Ihre Idempotenztabelle über einen eindeutigen Index für die Ereignis-ID
verfügt Häufige Fehler
Zugriff auf die Erfolgsseite gewähren. Benutzer, die es nie erreichen, zahlten trotzdem, und Benutzer, die es direkt besuchen, zahlten nie.
Analysieren des Körpers vor der Signaturüberprüfung. Für die Verifizierung werden die Rohbytes benötigt. Verwenden Siereq.text() und nichts anderes zuerst.
Vertrauen Sie auf den Preis des Kunden. Suchen Sie immer serverseitig nach der Kennung nach Preisen.
Gibt 200 zurück, wenn der Handler fehlschlägt. Stripe behandelt 2xx als Erfolg und stoppt den Wiederholungsversuch, sodass das Ereignis dauerhaft verloren geht. Geben Sie 500 zurück und lassen Sie es erneut versuchen.
Widerruf des Zugriffs bei der ersten fehlgeschlagenen Zahlung. Karten fallen aus vorübergehenden Gründen aus. Folgen Sie stattdessen dem Mahnverfahren von Stripe.
Fazit
Eine korrekte Stripe-Integration beruht auf einigen Regeln:Erstellen Sie serverseitig Sitzungen mit von Ihnen kontrollierten Preisen, behandeln Sie den Webhook als einzige Quelle der Wahrheit, überprüfen Sie Signaturen anhand des Rohtexts, machen Sie die Erfüllung idempotent mit einer eindeutigen Einschränkung für die Ereignis-ID und geben Sie nicht 2xx zurück, wenn die Verarbeitung fehlschlägt, damit Stripe es erneut versucht. Übergeben Sie die Abonnementverwaltung an das Abrechnungsportal, anstatt es neu zu erstellen. Machen Sie diese richtig und Zahlungen werden zum am wenigsten ereignisreichen Teil Ihrer Bewerbung.
🔗 Share this article
✍️ Leave a Comment