Webhooks erklärt: Signierte Payloads und zuverlässige Retries
Sie liefern eine Zahlungsintegration aus, der Anbieter feuert einen Webhook ab und Ihr Endpunkt gibt 200 zurück – doch drei Stunden später meldet der Support fehlende Bestellungen. Oder schlimmer: Dasselbe Event kommt viermal an und der Kunde wird doppelt belastet. Webhooks sehen einfach aus (ein HTTP-POST an Ihre URL), aber die schwierigen Teile sind Vertrauen und Zustellgarantien.
Dieser Leitfaden behandelt beides: wie Sie mit signierten Payloads überprüfen, dass eine Nutzlast tatsächlich vom Absender stammt, und wie Sie ein Retry-Modell aufbauen, das Ausfälle, Timeouts und doppelte Zustellungen übersteht.
Was ein Webhook tatsächlich ist
Ein Webhook ist ein umgekehrter API-Aufruf. Statt dass Ihr Server jede Minute GET /events abfragt, schickt der Anbieter eine HTTP-Anfrage an eine von Ihnen registrierte URL, wenn etwas passiert. Die Nutzlast ist normalerweise JSON, die Methode fast immer POST, und die von Ihnen zurückgegebene Antwort ist ein Signal: 2xx bedeutet „empfangen“, alles andere bedeutet „später erneut versuchen“.
Dieser letzte Satz ist der gesamte Vertrag. Alles andere – Signaturen, Retries, Reihenfolge – existiert, um diesen Vertrag über ein unzuverlässiges Netzwerk sicher zu machen.
Warum unsignierte Webhooks gefährlich sind
Ihr Webhook-Endpunkt ist eine öffentliche URL. Jeder, der sie entdeckt, kann eine gefälschte Nutzlast senden, die behauptet, eine Bestellung sei bezahlt. Wenn Sie dem Body ohne Verifikation vertrauen, haben Sie eine offene Tür in Ihre Abrechnungslogik gebaut.
IP-Allowlists helfen ein wenig, brechen aber hinter CDNs, Serverless-Egress und Anbieter-IP-Änderungen. Die robuste Antwort ist ein gemeinsames Geheimnis plus eine HMAC-Signatur über den rohen Request-Body.
Wie signierte Payloads funktionieren
Der Anbieter und Sie teilen einen geheimen Schlüssel. Bei jeder Zustellung berechnet der Anbieter einen HMAC (normalerweise SHA-256) über den rohen Body – manchmal über einen Zeitstempel plus den Body – und sendet den Hex-Digest in einem Header wie X-Signature. Sie berechnen denselben Digest mit Ihrer Kopie des Geheimnisses neu und vergleichen.
Zwei Details sind wichtiger, als viele erwarten:
- Signieren Sie die rohen Bytes. Wenn Ihr Framework JSON parst und neu serialisiert, ändern sich Schlüsselreihenfolge und Leerzeichen, und der Digest stimmt nicht überein. Lesen Sie zuerst den rohen Body.
- Vergleichen Sie in konstanter Zeit. Ein naives
==verrät Timing-Informationen. Verwenden Sie eine Funktion für konstante Zeitvergleiche aus der Krypto-Bibliothek Ihrer Sprache.
Ein minimales Verifikationsbeispiel (Node.js)
const crypto = require('crypto');
function verifySignature(rawBody, headerSig, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // roher Buffer oder String, kein geparstes JSON
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(headerSig || '', 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Dasselbe Muster funktioniert in Python mit hmac.compare_digest, in Go mit hmac.Equal und in Java mit MessageDigest.isEqual. Das Prinzip ist überall identisch: HMAC über rohe Bytes, Vergleich in konstanter Zeit.
Replay-Schutz mit Zeitstempeln
Eine gültige Signatur beweist, dass die Nutzlast von jemandem mit dem Geheimnis stammt – aber eine abgefangene Anfrage kann endlos wiederholt werden. Anbieter, denen das wichtig ist, fügen einen Zeitstempel in den signierten String ein, zum Beispiel t=1712345678,v1=<hex>. Auf Ihrer Seite:
- Parsen Sie den Zeitstempel aus dem Header.
- Lehnen Sie die Anfrage ab, wenn sie älter als ein Toleranzfenster ist (oft 5 Minuten).
- Berechnen Sie den HMAC über
timestamp + "." + rawBodyneu.
Dies begrenzt den Schaden einer geleakten Anfrage auf wenige Minuten statt für immer.
Retries entwerfen, die tatsächlich zustellen
Netzwerke fallen aus, Ihr Dienst startet neu, Deployments rollen Pods. Ein ernsthafter Anbieter wiederholt fehlgeschlagene Zustellungen nach einem Zeitplan. Ein ernsthafter Empfänger muss davon ausgehen, dass jedes Event mehr als einmal ankommen kann.
Retry-Regeln auf Senderseite
- Behandeln Sie jede Nicht-2xx-Antwort, jeden Verbindungsfehler oder Timeout als Fehler.
- Wiederholen Sie mit exponentiellem Backoff und Jitter – zum Beispiel 1s, 5s, 30s, 5m, 30m, 2h – damit ein Anbieterausfall bei der Wiederherstellung keine thundering herd erzeugt.
- Begrenzen Sie die Anzahl der Versuche (üblich sind 5–10) und protokollieren Sie den endgültigen Fehler, damit der Benutzer oder Betreiber ihn sehen kann.
- Setzen Sie ein kurzes Client-Timeout (5–10 Sekunden). Ein Webhook-Handler sollte schnell bestätigen und die Arbeit asynchron erledigen.
Regeln auf Empfängerseite
- Erst verifizieren, dann parsen. Lehnen Sie ungültige Signaturen mit 401 ab, bevor Sie Ihre Datenbank berühren.
- Event persistieren, dann 200 zurückgeben. Schreiben Sie die rohe Nutzlast und Event-ID in eine Queue oder Tabelle, antworten Sie sofort und verarbeiten Sie in einem Worker. Wenn die Verarbeitung fehlschlägt, wiederholen Sie intern.
- Machen Sie Handler idempotent. Verwenden Sie die Event-ID des Anbieters als eindeutigen Schlüssel. Wenn sie bereits existiert, geben Sie 200 zurück, ohne Seiteneffekte erneut auszuführen.
- Geben Sie Fehler ehrlich zurück. Wenn Ihre Datenbank ausgefallen ist, geben Sie 500 zurück, damit der Anbieter es erneut versucht – geben Sie nicht 200 zurück und verwerfen Sie das Event stillschweigend.
| Antwort | Bedeutung für den Sender | Was als Nächstes passiert |
|---|---|---|
| 200 / 204 | Zugestellt | Kein Retry |
| 400 | Fehlerhaft oder abgelehnt | Normalerweise kein Retry; zum Debuggen protokollieren |
| 401 | Signatur ungültig | Kein Retry; Geheimnis untersuchen |
| 429 | Rate limitiert | Retry mit Backoff |
| 5xx / Timeout | Temporärer Fehler | Retry mit Backoff |
Idempotenz in der Praxis
Idempotenz ist hier kein Schlagwort – es ist das Einzige, was zwischen einem Retry und einer Doppelbelastung steht. Das Muster:
INSERT INTO webhook_events (event_id, payload, received_at)
VALUES ($1, $2, now())
ON CONFLICT (event_id) DO NOTHING;
Wenn das Insert null Zeilen betrifft, haben Sie dieses Event bereits gesehen; bestätigen Sie und machen Sie weiter. Das funktioniert auch bei gleichzeitigen Zustellungen, weil die Unique-Constraint sie auf Datenbankebene serialisiert.
Für Seiteneffekte, die genau einmal passieren müssen (E-Mail senden, Karte belasten), geben Sie Ihren eigenen Idempotenzschlüssel stromabwärts weiter – die meisten Zahlungs- und E-Mail-APIs akzeptieren einen.
Reihenfolge, Debugging und Observability
Gehen Sie nicht davon aus, dass Events in Reihenfolge ankommen. Retries können Zustellungen umordnen, und parallele Worker tun das auch. Wenn Reihenfolge wichtig ist, fügen Sie eine Sequenznummer oder einen Zeitstempel in die Nutzlast ein und gleichen Sie den Zustand ab, anstatt Deltas blind anzuwenden.
Protokollieren Sie jeden Zustellversuch mit Event-ID, Signaturergebnis, Antwortcode und Verarbeitungszeit. Wenn ein Kunde sagt „meine Bestellung ist nie angekommen“, möchten Sie in Sekunden antworten können: Wurde der Webhook empfangen, war die Signatur gültig, war die Verarbeitung erfolgreich? Eine strukturierte Logzeile pro Event reicht aus.
Bei Endpunkten mit viel Traffic achten Sie nach einem Deployment auf Retry-Stürme. Ein schneller Weg, sie zu erkennen, ist die Betrachtung der Anfragemuster in Ihren Access-Logs – eine Spitze identischer Pfade von einer IP mit 5xx-Antworten ist eine klassische Retry-Schleife. Tools wie der Nginx-Log-Analyzer machen dieses Muster offensichtlich, ohne dass Sie eine Abfrage von Hand schreiben müssen.
Häufige Fehler, die Sie vermeiden sollten
- JSON parsen, bevor die Signatur verifiziert wurde.
- Dasselbe Geheimnis in allen Umgebungen verwenden oder es nie rotieren.
- Schwere Arbeit (PDF-Generierung, Drittanbieter-Aufrufe) im Request-Handler erledigen.
- Bei Verarbeitungsfehlern 200 zurückgeben, sodass der Anbieter nie erneut versucht.
- Dem Betrag oder Status in der Nutzlast vertrauen, ohne die Ressource bei Bedarf erneut von der API des Anbieters abzurufen.
FAQ
Wie verifiziere ich eine Webhook-Signatur, wenn der Body JSON ist?
Lesen Sie den rohen Request-Body als Bytes, bevor Sie irgendein JSON parsen, berechnen Sie den HMAC über diese Bytes und vergleichen Sie ihn mit dem Header mithilfe einer Funktion für konstante Zeit. Neu serialisiertes JSON erzeugt einen anderen Digest.
Wie oft sollte ein Webhook wiederholt werden?
Es gibt keine universelle Zahl, aber 5–10 Versuche über mehrere Stunden mit exponentiellem Backoff und Jitter sind üblich. Der wichtige Teil ist, dass Empfänger idempotent sind, sodass Retries auch dann sicher sind, wenn sie häufig auftreten.
Welchen Statuscode sollte mein Webhook-Endpunkt zurückgeben?
Geben Sie 2xx erst zurück, nachdem Sie das Event dauerhaft gespeichert haben. Geben Sie 401 bei ungültigen Signaturen zurück, 400 bei fehlerhaften Nutzlasten und 5xx oder 429, wenn Sie möchten, dass der Sender es erneut versucht.
Wenn Sie Webhook-Payloads testen oder das empfangene JSON protokollieren, ist der JSON-Formatter eine schnelle Möglichkeit, ein abgefangenes Event hübsch zu formatieren und zu validieren, bevor Sie Parsing-Logik darum herum aufbauen.