Webhooks erklärt: Signierte Payloads und zuverlässige Retries

Backend2026-10-23TryQuickToolBox

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:

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:

  1. Parsen Sie den Zeitstempel aus dem Header.
  2. Lehnen Sie die Anfrage ab, wenn sie älter als ein Toleranzfenster ist (oft 5 Minuten).
  3. Berechnen Sie den HMAC über timestamp + "." + rawBody neu.

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

Regeln auf Empfängerseite

  1. Erst verifizieren, dann parsen. Lehnen Sie ungültige Signaturen mit 401 ab, bevor Sie Ihre Datenbank berühren.
  2. 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.
  3. 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.
  4. 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.
AntwortBedeutung für den SenderWas als Nächstes passiert
200 / 204ZugestelltKein Retry
400Fehlerhaft oder abgelehntNormalerweise kein Retry; zum Debuggen protokollieren
401Signatur ungültigKein Retry; Geheimnis untersuchen
429Rate limitiertRetry mit Backoff
5xx / TimeoutTemporärer FehlerRetry 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

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.