Stripe Webhooks: einrichten, verifizieren und testen
Stripe-Webhooks-Anleitung
Stripe Webhooks: Endpunkt, Signatur, Tests mit der CLI | We.Inc
Richte einen Stripe-Webhook-Endpunkt ein, prüfe Stripe-Signature mit dem Raw Body, teste mit stripe listen und stripe trigger und führe Checkout-Bestellungen genau einmal aus.
Ein Stripe-Webhook ist ein HTTPS-Endpunkt auf deinem Server, an den Stripe Event-JSON per POST sendet, etwa checkout.session.completed nach einer Zahlung. Dein Handler muss den Stripe-Signature-Header mit dem whsec_-Secret des Endpunkts gegen den unveränderten Raw Body prüfen, schnell einen 2xx-Status zurückgeben und damit klarkommen, dass dasselbe Event mehr als einmal ankommt.
Das Wichtigste zu Stripe-Webhooks (abgeglichen mit der Stripe-Dokumentation, Oktober 2026)
Die meisten kaputten Stripe-Integrationen scheitern an einer von drei Stellen: Das Framework hat den Body vor der Prüfung geparst, der Handler hat langsame Arbeit erledigt und ist in ein Timeout gelaufen, oder ein wiederholtes Event hat eine Bestellung doppelt ausgeführt. Diese Anleitung zeigt einen Handler, der alle drei vermeidet, und unten einen Checker, der einen Stripe-Signature-Header in deinem Browser prüft, damit du genau siehst, was die Library berechnet.
Lege in Workbench (oder über die API) ein Event-Ziel an, das auf deine HTTPS-URL zeigt, und kopiere dessen whsec_-Signing-Secret.
Lies in deiner Route den Raw Body und übergib ihn zusammen mit dem Stripe-Signature-Header und dem Secret an constructEvent.
Verzweige nach event.type, stelle die Arbeit in eine Queue und gib sofort 200 zurück.
Teste lokal mit stripe listen --forward-to und löse dann Events mit stripe trigger aus.
Das Secret, das stripe listen ausgibt, unterscheidet sich vom Secret deines Endpunkts im Dashboard. Verwende in jeder Umgebung das richtige.
Test- und Live-Endpunkte haben getrennte Secrets, selbst wenn die URL gleich ist.
Abonniere nur die Event-Typen, die du auch verarbeitest; auf alles zu hören erzeugt nur unnötige Last.
Nimm die Webhook-Route in Frameworks wie Rails oder Django von der CSRF-Middleware aus.
Stripe wertet einen Redirect als fehlgeschlagene Zustellung. Registriere die endgültige URL, inklusive des abschließenden Schrägstrichs, den dein Server erwartet.
Häufige Fragen
Warum bekomme ich "No signatures found matching the expected signature"?
Fast immer, weil der Body vor der Prüfung verändert wurde, zum Beispiel durch express.json() oder einen Body-Parser, der das JSON neu kodiert. Binde express.raw({ type: "application/json" }) nur an die Webhook-Route oder lies request.text() in einem Next.js-Route-Handler. Ein falsches Secret führt zur selben Meldung.
Welche Events brauche ich für Stripe Checkout?
checkout.session.completed für Kartenzahlungen, dazu checkout.session.async_payment_succeeded für verzögerte Methoden wie Lastschriften. Stripe empfiehlt außerdem, checkout.session.async_payment_failed zu verarbeiten, damit du den Kunden informieren kannst. Prüfe vor dem Ausführen den payment_status der Session.
Kann ich mich statt auf einen Webhook auf die Erfolgsseite verlassen?
Nein. Laut Stripes eigener Dokumentation kann ein Kunde bezahlen und den Tab schließen, bevor deine Erfolgsseite lädt. Nutze die Erfolgsseite gern für eine schnelle Bestätigung, aber der Webhook muss die Ausführung ebenfalls auslösen, und beide Wege müssen gefahrlos zweimal laufen können.
Kommen Stripe-Events in der richtigen Reihenfolge an?
Stripe garantiert keine Reihenfolge. invoice.paid kann vor customer.subscription.created ankommen. Speichere Event-IDs, um Duplikate zu erkennen, und hol das zugehörige Objekt über die API, wenn du seinen aktuellen Stand brauchst.
Wie sende ich ein fehlgeschlagenes Event erneut?
Klicke im Dashboard beim Event auf Resend (funktioniert bis 15 Tage nach der Erstellung) oder führe mit der CLI stripe events resend <event_id> --webhook-endpoint=<endpoint_id> aus (bis 30 Tage). Automatische Wiederholungen laufen trotzdem weiter.
Die Raw-Body-Regel
Stripe berechnet einen HMAC SHA-256 über den Zeitstempel, einen Punkt und exakt die Bytes, die es gesendet hat. Wenn irgendetwas dieses JSON parst und wieder in einen String verwandelt, können sich Leerzeichen oder die Reihenfolge der Keys verschieben, und die Signatur passt nicht mehr.
In Express bedeutet das express.raw auf genau dieser Route, eingebunden bevor ein globaler JSON-Parser läuft. In einem Next.js-App-Router-Handler rufst du await request.text() auf und übergibst den String; niemals request.json().
Erst antworten, dann arbeiten
Stripe wartet nur kurz auf eine Antwort, und mit einer Checkout-success_url hält es den Kunden bis zu 10 Sekunden fest, während es auf deine Antwort auf checkout.session.completed wartet. Speichere die Event-ID, schiebe den Job in eine Queue, gib 200 zurück und versende dann im Hintergrund E-Mails und aktualisiere Datensätze.
Genau einmal ausführen
Deine Ausführungsfunktion kann für eine Checkout Session mehrmals laufen, manchmal sogar gleichzeitig, durch eine Wiederholung plus die Erfolgsseite. Speichere innerhalb einer Transaktion ein Erledigt-Flag mit der Session-ID als Schlüssel und überspringe die Arbeit, wenn es bereits gesetzt ist.
Handler-Code
Express mit stripe-node
express.raw ist nur auf dieser Route eingebunden, sodass der Body als Originalbytes bei constructEvent ankommt.
Next.js-App-Router-Route-Handler
request.text() liefert den ungeparsten Body. Wirft constructEvent einen Fehler, sind Signatur oder Secret falsch.
Testen mit der Stripe CLI
Events lokal weiterleiten und auslösen
listen gibt für diese Sitzung ein whsec_-Secret aus. Trag es in deine lokale Umgebung ein und löse dann in einem zweiten Terminal Events aus.
Events, die du verarbeiten solltest
Häufige Stripe-Event-Typen und was zu tun ist
Event
Wann es ausgelöst wird
Typische Aktion
Ein Kunde hat Checkout oder einen Payment Link abgeschlossen
Ausführen, wenn payment_status nicht unpaid ist
Eine verzögerte Zahlungsmethode wurde später bestätigt
Jetzt ausführen
Eine verzögerte Zahlung ist fehlgeschlagen
Dem Kunden eine E-Mail senden
Ein PaymentIntent wurde bezahlt
Die Bestellung als bezahlt markieren (eigene Zahlungsabläufe)
Eine Abo-Rechnung wurde bezahlt
Zugang für den Zeitraum verlängern
Stripe-Signature-Checker
Füge einen Stripe-Signature-Header, den Raw Body und dein whsec_-Secret ein, um dieselbe Prüfung zu sehen, die die Libraries ausführen. Alles läuft mit Web Crypto in deinem Browser und es wird nichts gesendet. Verwende ein Secret aus dem Testmodus, niemals ein Live-Secret.
Stripe-Signature-Header
Raw Request Body
Endpunkt-Secret (whsec_...)
Toleranz in Sekunden
Diesen Body jetzt signieren
Prüfen
Ergebnis
Der Header hat keinen t=-Zeitstempel.
Der Header hat keine v1=-Signatur.
Erwartetes v1: {v}
Die Signatur stimmt.
Die Signatur stimmt nicht: Der Body wurde verändert oder das Secret ist falsch.
Der Zeitstempel ist {n} Sekunden alt und liegt innerhalb der Toleranz.
Der Zeitstempel ist {n} Sekunden alt und liegt außerhalb der Toleranz: Eine Library würde ihn ablehnen.