Verifying Webhook Signatures
Every webhook delivery is signed with HMAC-SHA256. Verifying the signature ensures the request genuinely came from Hermon and has not been tampered with.
How Signatures Work
Hermon generates a signature for each delivery using your webhook secret and the raw request body:
secret — Your webhook secret shown when you created the endpoint. Format: whsec_<64 hex chars>
body_string — The raw JSON request body as a string. Do not parse it first. Any whitespace difference will cause the signature to fail.
Verification Examples
Select your language to see a complete, production-ready verification example:
const crypto = require("crypto");
function verifyWebhook(secret, rawBody, signatureHeader) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
// Express example — use express.raw() to get the raw body buffer
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-hermon-signature"];
if (!verifyWebhook("whsec_your_secret", req.body, sig)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
// process event...
res.status(200).send("OK");
});The Signature Header
The signature is delivered in the X-Hermon-Signature request header in the format:
Receiving the Raw Body
The signature is computed over the raw request body — you must access the body before it is parsed. Many frameworks offer a way to do this:
Express (Node.js) — Use express.raw({ type: 'application/json' }) middleware on your webhook route.
Next.js — Export a custom config with api: { bodyParser: false } and read req.body as a buffer.
Flask (Python) — Use request.data or request.get_data() before accessing request.json.
Finding Your Secret
Your webhook secret is shown once when you create an endpoint via the dashboard or API. It is stored hashed and cannot be retrieved afterwards. If you lose it, use the Regenerate Secret endpoint to generate a new one — this will invalidate the old secret immediately.