Zum Inhalt springen
Zyloft

Stripe-Webhooks in Next.js richtig verarbeiten

Wenn ein Webhook-Handler patzt, zahlen Kunden, ohne Zugang zu bekommen. So verarbeiten Sie Stripe-Webhooks im Next.js App Router sicher: Signatur, Idempotenz, Reihenfolge und lokale Tests.

Aktualisiert am 10. November 20264 Min. Lesezeit

Über Webhooks erfährt Ihre App, dass eine Zahlung eingegangen ist, ein Abo verlängert wurde oder ein Kunde eine Zahlung anficht. Läuft der Handler nicht zuverlässig, stimmen Zugänge, Rechnungen und E-Mails nicht mehr mit dem überein, was in Stripe passiert ist. Die gute Nachricht: Mit fünf Regeln ist ein Webhook-Handler robust.

Wie Stripe Webhooks zustellt

  • Stripe sendet jedes Ereignis als HTTPS-POST mit einem JSON-Event an Ihren Endpunkt.
  • Als zugestellt gilt ein Ereignis nur, wenn Ihr Endpunkt mit einem 2xx-Statuscode antwortet.
  • Schlägt die Zustellung fehl, wiederholt Stripe sie im Live-Modus mit exponentiellem Backoff bis zu drei Tage lang.
  • Die Reihenfolge der Ereignisse ist nicht garantiert, und dasselbe Ereignis kann mehr als einmal ankommen.

Der Route Handler im App Router

Im App Router ist der Webhook ein Route Handler, zum Beispiel unter app/api/stripe/webhook/route.ts. Entscheidend ist, den Rohtext der Anfrage mit request.text() zu lesen: Wird der Body vorher als JSON geparst und wieder serialisiert, ändern sich die Bytes, und die Signaturprüfung schlägt fehl.

ts
// app/api/stripe/webhook/route.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  // The raw body, byte for byte: parsing it first breaks the signature check.
  const body = await request.text();
  const signature = request.headers.get("stripe-signature");
  if (!signature) return new Response("Missing signature", { status: 400 });

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch {
    return new Response("Invalid signature", { status: 400 });
  }

  await handleEvent(event); // idempotent, see below
  return new Response(null, { status: 200 });
}

constructEvent nutzt die Krypto-Funktionen von Node.js. Läuft der Handler in der Edge-Runtime, verwenden Sie stattdessen constructEventAsync.

Regel 1: Die Signatur immer prüfen

Ihr Webhook-Endpunkt ist öffentlich erreichbar. Nur die Signatur beweist, dass eine Anfrage wirklich von Stripe kommt. Stripe schickt sie im Header Stripe-Signature, zusammen mit einem Zeitstempel. Die offiziellen Bibliotheken prüfen beides und lehnen standardmäßig Anfragen ab, die älter als fünf Minuten sind. Das schützt vor Replay-Angriffen. Setzen Sie diese Toleranz nie auf 0, denn damit schalten Sie die Prüfung ab.

Jeder Endpunkt hat ein eigenes Geheimnis, das mit whsec_ beginnt. Die Stripe CLI erzeugt für lokale Tests ein anderes Geheimnis als der Endpunkt im Dashboard. Halten Sie beide in getrennten Umgebungsvariablen.

Regel 2: Duplikate abfangen

Weil ein Ereignis mehrfach ankommen kann, muss jede Verarbeitung idempotent sein: Zweimal dasselbe Ereignis darf nicht zweimal freischalten, zwei Rechnungen erzeugen oder zwei E-Mails senden. Am einfachsten speichern Sie die IDs verarbeiteter Ereignisse mit einem eindeutigen Schlüssel, in derselben Transaktion wie die eigentliche Änderung.

sql
create table processed_events (
  id text primary key,                         -- evt_…
  processed_at timestamptz not null default now()
);

-- In the handler, inside one transaction with the actual work:
insert into processed_events (id) values ($1)
on conflict (id) do nothing;
-- 0 rows inserted → already handled: answer 200 and stop.

In seltenen Fällen erzeugt Stripe für denselben Vorgang zwei getrennte Ereignisse mit unterschiedlichen IDs. Wo das zählt, erkennen Sie Duplikate zusätzlich an der ID des Objekts in data.object zusammen mit dem Ereignistyp.

Regel 3: Keine Reihenfolge voraussetzen

Beim Abschluss eines Abos entstehen mehrere Ereignisse, die in beliebiger Reihenfolge ankommen können. Bauen Sie den Handler deshalb so, dass er den aktuellen Zustand nicht aus der Reihenfolge ableitet: Lesen Sie im Zweifel den aktuellen Stand des Objekts über die Stripe-API nach, bevor Sie Ihren Datensatz aktualisieren.

Regel 4: Schnell antworten, später arbeiten

Antworten Sie mit 2xx, bevor langsame Arbeit beginnt. Dauert die Antwort zu lange, wertet Stripe die Zustellung als gescheitert und versucht es erneut, obwohl Ihr Code vielleicht schon gearbeitet hat. Aufwendiges wie E-Mails, PDFs oder Aufrufe anderer Dienste gehört in eine Warteschlange oder einen Hintergrundjob.

Regel 5: Nur die nötigen Ereignisse abonnieren

Abonnieren Sie im Dashboard nur die Ereignistypen, die Ihre App wirklich verarbeitet, zum Beispiel checkout.session.completed, customer.subscription.updated und invoice.payment_failed. Alles andere erzeugt Last ohne Nutzen.

Lokal testen mit der Stripe CLI

Mit der Stripe CLI leiten Sie Ereignisse an Ihren lokalen Server weiter und lösen Test-Ereignisse gezielt aus. stripe listen zeigt beim Start das Signatur-Geheimnis an, das Sie lokal verwenden.

bash
stripe listen --forward-to localhost:3000/api/stripe/webhook
stripe trigger checkout.session.completed

Checkliste vor dem Livegang

  • Rohtext mit request.text() gelesen und Signatur mit constructEvent geprüft
  • Getrennte Geheimnisse für CLI, Test- und Live-Endpunkt
  • Verarbeitete Event-IDs mit eindeutigem Schlüssel gespeichert
  • Keine Annahmen über die Reihenfolge der Ereignisse
  • 2xx sofort, langsame Arbeit im Hintergrund
  • Nur benötigte Ereignistypen abonniert
  • Fehlgeschlagene Zustellungen werden bemerkt, nicht erst durch Kundenbeschwerden

Fehlgeschlagene Zustellungen im Blick behalten

Stripe zeigt im Dashboard, welche Zustellungen gescheitert sind. Empfangen Sie Webhooks von mehreren Anbietern, etwa zusätzlich von GitHub oder Shopify, lohnt sich eine zentrale Stelle, die jeden Webhook aufzeichnet, signiert weiterleitet und gescheiterte Zustellungen erneut senden kann.

Stand: November 2026, auf Grundlage der Stripe-Dokumentation zu Webhooks. Details wie Wiederholungszeiträume können sich ändern; maßgeblich ist die aktuelle Dokumentation von Stripe.

3 Min. Lesezeit

Next.js SaaS Boilerplate: kaufen oder selbst bauen?

Login, Zahlungen, E-Mails, Datenbank und Tests braucht jedes SaaS, bevor die erste eigene Funktion fertig ist. Wann ein fertiges Boilerplate Zeit spart und woran Sie ein gutes erkennen.

3 Min. Lesezeit

Junior-Entwickler: Was im ersten Job wirklich zählt

Tutorials und Rätselaufgaben trainieren Syntax und Algorithmen. Im ersten Job zählt etwas anderes: Tickets verstehen, planen, testen und mit Reviews umgehen. So üben Sie genau das.