Anleitung: WeZend-Webhook-Signaturen in Express verifizieren (mit Code)
Ein vollständiger, funktionierender Express-Endpoint, der Zustell-Webhooks empfängt und alles ablehnt, was nicht wirklich von WeZend stammt.
Die Webhooks von WeZend gelten pro Nachricht: Ihr übergebt webhook_url in der Send-Request selbst (siehe Webhooks) — und das hier ist die Empfängerseite davon.
Der Endpoint
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.raw({ type: "application/json" })); // für die Verifizierung brauchen wir den Raw-Body
app.post("/hooks/wezend", (req, res) => {
const signature = req.get("X-ZafeConnect-Signature");
const timestamp = req.get("X-ZafeConnect-Timestamp");
const rawBody = req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", process.env.WEZEND_WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const isValid =
signature &&
expected.length === signature.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
if (!isValid) return res.status(401).send("invalid signature");
res.sendStatus(200); // schnell antworten — die eigentliche Arbeit danach
const payload = JSON.parse(rawBody);
processDeliveryEvent(payload).catch(console.error);
});
Drei Dinge, die man richtig machen sollte
- Nutzt den Raw-Body für die Signaturprüfung, nicht das geparste/neu serialisierte JSON — beim erneuten Serialisieren können sich Whitespace oder Schlüsselreihenfolge subtil ändern und den HMAC-Vergleich zerstören.
- Antwortet mit
2xx, bevor ihr langsame Arbeit erledigt. Eine fehlgeschlagene oder langsame Antwort löst einen Retry aus (30 Sek., 5 Minuten, 30 Minuten) — sinnvoll bei einem echten Ausfall, Verschwendung bei einem Handler, der einfach nur langsam ist. - Die Header-Namen lauten weiterhin
X-ZafeConnect-*, nichtX-WeZend-*— ein Überbleibsel des ursprünglichen Plattformnamens, das im Backend noch nicht umbenannt wurde. Übernehmt den Header-Namen exakt wie oben gezeigt; geht nicht davon aus, dass er der aktuellen Marke folgt.
Woher das Signing Secret kommt
Ihr findet (und rotiert) es unter Einstellungen → Webhook-Secret, gestützt auf GET /v1/webhook-secret und POST /v1/webhook-secret/rotate.