14. August 2026
Dein Zahlungs-Webhook glaubt jedem
Der Testkauf läuft, Pro ist freigeschaltet, alles gut. Nur: Die Adresse, die deinen Kunden freischaltet, ist öffentlich. Ein Test mit curl zeigt in 30 Sekunden, ob sie jedem glaubt.
Der Testkauf ist durch. Im Dashboard steht die Zahlung, in deiner Datenbank steht is_pro = true, in der App ist alles freigeschaltet. Es funktioniert. Für dieses Gefühl hast du zwei Tage gearbeitet.
Es funktioniert auch für alle anderen. Und zwar ohne Zahlung.
Die Adresse muss öffentlich sein
Ein Webhook ist eine Adresse in deinem Backend, die dein Zahlungsanbieter aufruft, wenn etwas passiert. Kauf erfolgreich, Abo verlängert, Rückerstattung erteilt. Diese Adresse muss von außen erreichbar sein, sonst kommt die Nachricht nie an. Es gibt keine Anmeldung davor, kein Passwort, keinen Nutzer.
Was heißt: Sie ist nicht nur für Stripe erreichbar. Sie ist für jeden erreichbar, der die Adresse kennt.
Die Version, die am Ende live geht
Die Signaturprüfung schreibt dir jedes aktuelle Modell mit. Live geht trotzdem oft diese Version hier, weil die Prüfung unterwegs wieder rausgeflogen ist:
app.post('/api/webhook', async (req, res) => {
const event = req.body;
if (event.type === 'checkout.session.completed') {
await db.setPro(event.data.object.customer_email);
}
res.sendStatus(200);
});
Das ist funktionierender Code. Der Testkauf läuft damit durch. Und der Code hat keine Ahnung, wer ihn gerade aufgerufen hat. Er liest, was im Paket steht, und glaubt es.
Der Test dauert 30 Sekunden
Schick deinem eigenen Endpunkt ein Paket, das du dir selbst ausgedacht hast:
curl -X POST https://deine-app.de/api/webhook \
-H "Content-Type: application/json" \
-d '{"type":"checkout.session.completed",
"data":{"object":{"customer_email":"test@example.com"}}}'
Danach schaust du in deine Datenbank. Steht der Nutzer auf Pro, ist der Fall klar. Kein Kauf, keine Zahlung, keine Anmeldung. Nur eine Zeile im Terminal.
Das ist kein Angriff und kein Trick. Das ist genau der Aufruf, den dein Zahlungsanbieter macht, nur ohne den Teil, der ihn beweist.
Warum die Prüfung trotzdem fehlt
Die Prüfung braucht ein Geheimnis, das nur in deinem Anbieter-Dashboard steht. Die KI kommt da nicht ran. Sie schreibt den Code, anschließen musst du ihn. Und beim Anschließen schlägt die Prüfung zuerst einmal fehl, aus dem Grund, der weiter unten steht. Die schnellste Reparatur ist, sie wieder rauszunehmen.
Danach läuft es. Im Test läuft es perfekt, denn im Test ruft niemand die Adresse auf, der es nicht darf. Und “läuft wieder” hält man für “ist richtig”.
Das ist die Sorte Lücke, die keine Fehlermeldung erzeugt. Nichts ist rot, nichts stürzt ab, kein Log schlägt aus. Du merkst es an dem Tag, an dem dir jemand erzählt, dass er dein Produkt gratis nutzt.
Was stattdessen dahingehört
Der Anbieter unterschreibt jedes Paket. Stripe legt eine Stripe-Signature in den Kopf der Anfrage, und du prüfst sie gegen dein Geheimnis, so beschrieben in der Stripe-Doku:
app.post('/api/webhook',
express.raw({ type: 'application/json' }), // roher Body, kein JSON
(req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers['stripe-signature'],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.sendStatus(400); // gefälscht, raus hier
}
// ab hier ist bewiesen, wer geschickt hat
});
Drei Stolperstellen, in dieser Reihenfolge:
Der Body muss roh sein. Die Unterschrift gilt für die Bytes, die ankamen. Wenn dein Framework das Paket vorher in ein JSON-Objekt verwandelt, passt die Unterschrift nicht mehr, und die Prüfung schlägt fehl. Der Fehler sieht aus wie ein kaputter Webhook. Die verbreitete Reaktion darauf ist, die Prüfung wieder rauszunehmen, weil es dann ja wieder läuft. Damit bist du wieder am Anfang.
Das Geheimnis ist pro Endpunkt verschieden. Test und Produktion haben unterschiedliche. Wer eins kopiert und überall einsetzt, wundert sich später über Ausfälle in der Produktion.
Dasselbe Paket kommt mehrfach. Anbieter wiederholen die Zustellung, wenn deine Antwort ausbleibt oder zu lange dauert. Wenn dein Code bei jedem Eingang Guthaben gutschreibt, schreibt er es dreimal gut. Ereignis-ID speichern, bekannte ID ignorieren.
Die Ebene darüber
Auch mit geprüfter Unterschrift bleibt eine Frage offen: Wer entscheidet, dass jemand bezahlt hat?
Solange die App das entscheidet, weil der Kauf-Dialog “erfolgreich” zurückgegeben hat, verlagerst du die Entscheidung nur woandershin. Die Berechtigung gehört auf den Server, und die App fragt sie ab. Der Kauf ist ein Ereignis, kein Beweis.
Wo das gesammelt liegt
Signatur, roher Body, Wiederholungen, Berechtigung vom Server: Das sind vier Stellen, an denen es klemmt, und keine davon meldet sich von selbst. Dazu kommt der ganze Rest, der erst in Produktion auftaucht. Warum der Sandbox-Kauf grün ist und der echte rot. Was passiert, wenn eine Rückerstattung eintrifft. Wo Trial und Verlängerung auseinanderlaufen.
Der Payment-Guide hat genau diese Stellen. Die Grundlagen stehen in jeder Doku. Hier steht das, worüber man beim ersten Mal stolpert.
Heute reicht die eine Zeile mit curl. Schick dir dein eigenes Paket und schau nach, was danach in deiner Datenbank steht.
Diplom-Informatiker · 25 Jahre Entwicklung · LinkedIn
PDF plus Beispiele · 197 € · framework-unabhängig
Payment & Web-Checkout Best PracticeWeitere Artikel
7. August 2026
RLS an heißt nicht abgesichert
Das grüne Häkchen im Supabase-Dashboard sagt nur, dass ein Schloss dranhängt. Ob es zugesperrt ist, sagt es nicht. Ein Test mit einem zweiten Nutzer, der drei Minuten dauert.
31. Juli 2026
Lass dir's zeigen, nicht sagen
Die KI klingt gleich sicher, ob sie geprüft oder geraten hat. Vier konkrete Moves, die sie zum Beweis zwingen statt zur Behauptung.