Signature verification
Every webhook delivery is signed with HMAC-SHA256 using your endpoint secret. Verifying the signature before processing events is essential — it prevents replay attacks and spoofed payloads.
The signature header
The signature is delivered in the X-HLD-Signature-256 header in the format sha256=<hex_digest>. The digest is computed over the raw, unparsed request body.
Warning:Always verify against the raw bytes of the request body — not a parsed JSON object. Serialisation differences will cause valid signatures to fail.
Node.js
typescript
import crypto from 'crypto'
import type { Request, Response } from 'express'
export function webhookHandler(req: Request, res: Response) {
const signature = req.headers['x-hld-signature-256'] as string
const secret = process.env.HLD_WEBHOOK_SECRET!
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(req.rawBody) // express: use bodyParser with { verify: ... }
.digest('hex')
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Signature mismatch')
}
const event = JSON.parse(req.rawBody)
// handle event...
res.status(200).send('ok')
}Python
python
import hmac
import hashlib
from flask import request, abort
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
@app.route('/webhook', methods=['POST'])
def webhook():
sig = request.headers.get('X-HLD-Signature-256', '')
if not verify_signature(request.data, sig, HLD_WEBHOOK_SECRET):
abort(401)
event = request.get_json()
# handle event...
return 'ok', 200Replay protection
Every webhook payload includes a created_at timestamp. Reject events where created_at is more than 5 minutes in the past to protect against replay attacks.
typescript
const event = JSON.parse(req.rawBody)
const ageMs = Date.now() - new Date(event.created_at).getTime()
if (ageMs > 5 * 60 * 1000) {
return res.status(400).send('Stale event')
}