features
HMAC Verification
Automatically verify HMAC signatures from Stripe, GitHub, Shopify, and custom providers.
Webhook providers sign requests with a shared secret so the receiver can verify the request really came from them. HookSense verifies signatures automatically on every incoming request — and the same logic is the reference implementation you'll want to ship in your own handler.
Supported Providers
| Provider | Header | Algorithm |
|---|---|---|
| Stripe | Stripe-Signature | HMAC-SHA256 with timestamp (t=...,v1=...) |
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 hex, prefixed sha256= |
| Shopify | X-Shopify-Hmac-SHA256 | HMAC-SHA256 base64 |
| Custom | Auto-detected | HMAC-SHA256 hex |
Setup in HookSense
- Open your endpoint page and click the Shield icon in the toolbar
- Paste your signing secret (e.g.,
whsec_...for Stripe) - Select the provider or leave on Auto-detect
- Save — every new request will show a Verified or Invalid badge
HookSense uses constant-time comparison to prevent timing attacks. Unverified requests are still captured, but they're flagged so you can filter them out with the Verified only toggle.
Verifying in Your Own Handler
HookSense forwards webhooks to your server with the original signature header intact. Implement the same check on your side so production traffic is protected after you stop using HookSense as a proxy.
Stripe (Node.js)
import Stripe from "stripe";
const stripe = new Stripe(process.env.STRIPE_SECRET);
const secret = process.env.STRIPE_WEBHOOK_SECRET; // whsec_...
app.post("/webhooks/stripe", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["stripe-signature"];
try {
const event = stripe.webhooks.constructEvent(req.body, sig, secret);
// ... handle event.type
res.json({ received: true });
} catch (err) {
res.status(400).send(`Webhook Error: ${err.message}`);
}
});
Stripe's library checks both the signature and the timestamp (5-minute tolerance). Always pass the raw body — JSON parsing first will break verification.
GitHub (Node.js)
import crypto from "node:crypto";
const secret = process.env.GITHUB_WEBHOOK_SECRET;
function verify(req) {
const sig = req.headers["x-hub-signature-256"]; // "sha256=..."
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(req.rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Shopify (Node.js)
import crypto from "node:crypto";
const secret = process.env.SHOPIFY_WEBHOOK_SECRET;
function verify(req) {
const sig = req.headers["x-shopify-hmac-sha256"];
const expected = crypto
.createHmac("sha256", secret)
.update(req.rawBody, "utf8")
.digest("base64");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Generic / Custom (Node.js)
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signatureHeader),
Buffer.from(expected)
);
}
Stripe (Python / Flask)
import stripe
from flask import request, abort
@app.post("/webhooks/stripe")
def stripe_webhook():
payload = request.data # raw bytes
sig = request.headers.get("Stripe-Signature")
try:
event = stripe.Webhook.construct_event(payload, sig, os.environ["STRIPE_WEBHOOK_SECRET"])
except (ValueError, stripe.error.SignatureVerificationError):
abort(400)
# handle event["type"]
return "", 200
Common Pitfalls
- Parsing JSON before verifying — most frameworks do this by default. Use raw body middleware (
express.raw, Flaskrequest.data, etc.) on the webhook route only. - String comparison instead of constant-time — opens you to timing attacks. Always use
crypto.timingSafeEqual/hmac.compare_digest. - Using the wrong secret — Stripe has separate signing secrets per webhook endpoint and per environment (test/live). Match them carefully.
- Trusting unverified requests during dev — when HookSense shows "Invalid", check the secret first; never disable verification "just for now."
Verification is available on Hook plans and above.