WundertreOS

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:

text
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):

text
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

js
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

python
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:

AttemptWait before retry
1immediate
21 minute
35 minutes
415 minutes
51 hour
6 – 86 hours

After 8 failed attempts the event is marked failed and dropped.

Use X-Wunder-Delivery for dedupe

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.