Verify signatures
Prove a webhook really came from Nembol — copy-paste verifiers in four languages.
Every delivery carries:
X-Nembol-Signature: t=1784900412,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— Unix timestamp of the delivery attempt.v1— HMAC-SHA256, keyed with your endpoint's signing secret, over the string"<t>.<raw_body>". During a secret rotation window you may see twov1=entries — accept if any matches.
Verification steps: (1) read the raw request body — before any JSON
parsing or middleware transformation; (2) compute the expected HMAC;
(3) compare in constant time; (4) reject if |now − t| is more than 5
minutes (replay protection).
import crypto from 'node:crypto';
// Express: app.post('/webhook', express.raw({ type: 'application/json' }), handler)
export function verifyNembolSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(',').map((kv) => kv.split('=', 2)),
);
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return signatureHeader
.split(',')
.filter((kv) => kv.startsWith('v1='))
.some((kv) => {
const candidate = Buffer.from(kv.slice(3), 'hex');
const exp = Buffer.from(expected, 'hex');
return candidate.length === exp.length && crypto.timingSafeEqual(candidate, exp);
});
}The classic mistake: verifying against a re-serialized JSON body. Your framework's parsed-then-stringified JSON will not be byte-identical to what Nembol sent, and the signature won't match. Always keep the raw bytes.
Need help?
The API is in private beta — email us and a human replies, usually within one business day.