Verify webhook signatures
How WundertreOS signs webhook deliveries and how to verify them.
Every webhook delivery is signed with HMAC‑SHA256 using the subscription's secret. Verify the signature before trusting the payload.
How signing works
When dispatching a delivery, WundertreOS computes:
signature = HMAC_SHA256(secret, raw_request_body)and sends the hex digest in two headers (the values are identical — both are provided for Zapier REST Hooks compatibility):
X-Wunder-Signature: sha256=<64-char hex>
X-Hook-Signature: sha256=<64-char hex>The signature covers the exact raw bytes of the JSON request body. Re‑serializing the body before verifying will fail — always verify against the raw bytes you received.
Node.js example
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.WUNDERTRE_WEBHOOK_SECRET;
// Capture the raw body — required for signature verification.
app.post(
"/wundertre/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
const header = req.header("X-Wunder-Signature") ?? "";
const signature = header.replace(/^sha256=/, "");
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body) // Buffer of the raw body
.digest("hex");
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expected, "hex"),
);
if (!ok) return res.status(401).send("Invalid signature");
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.event_type, event.data);
res.status(200).send("ok");
},
);Python example
import hmac, hashlib
def verify(raw_body: bytes, header: str, secret: str) -> bool:
sig = (header or "").removeprefix("sha256=")
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)Retries and idempotency
Failed deliveries (non‑2xx response or a connection error) are retried with exponential backoff. The full schedule is 8 attempts:
| Attempt | Wait before retry |
|---|---|
| 1 | immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 15 minutes |
| 5 | 1 hour |
| 6 – 8 | 6 hours |
After 8 failed attempts the event is marked failed and dropped.
WundertreOS may retry a successful delivery if your endpoint times out before responding. Dedupe by the X-Wunder-Delivery header (which equals the event id) and return 200 as soon as you've persisted the event — process it asynchronously.
