Skip to main content

Verifying signatures

Every webhook is signed with HMAC-SHA256 using your client's webhook secret. The signed payload is `${timestamp}.${rawBody}` which prevents both tampering and replay attacks.

Node.js exampleโ€‹

webhook-handler.js
import { createHmac, timingSafeEqual } from 'crypto';

const SECRET = process.env.HELAMESH_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300; // 5 minutes

function verifySignature(header, rawBody) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=')),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!t || !v1) return false;

// Reject signatures outside the tolerance window
if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_SECONDS) return false;

const expected = createHmac('sha256', SECRET)
.update(`${t}.${rawBody}`)
.digest('hex');

return expected.length === v1.length &&
timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(v1, 'hex'));
}

// Express handler โ€” note rawBody is required, not parsed JSON
app.post('/webhooks/helamesh', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('X-HelaMesh-Signature');
if (!sig || !verifySignature(sig, req.body.toString())) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// Mark the order paid in your database
await markOrderPaid(event.invoiceId, event.txHash);
res.status(200).send('ok');
});
Verify against the RAW request body

You must verify against the raw request body, not the parsed JSON. JSON.stringify() is not deterministic across languages โ€” a re-serialised body will have a different hash and your signature will fail. Express users: use express.raw() middleware on the webhook route only. Next.js App Router: use await req.text() instead of await req.json(). FastAPI: use await request.body() instead of reading the pydantic model.

Python exampleโ€‹

views.py
import hmac
import hashlib
import time

def verify_helamesh_signature(header: str, raw_body: bytes, secret: str) -> bool:
try:
parts = dict(kv.split('=') for kv in header.split(','))
t, v1 = int(parts['t']), parts['v1']
except (KeyError, ValueError):
return False

if abs(int(time.time()) - t) > 300:
return False

expected = hmac.new(
secret.encode(),
f"{t}.{raw_body.decode()}".encode(),
hashlib.sha256,
).hexdigest()

return hmac.compare_digest(expected, v1)