Verify signatures

Prove a webhook really came from Nembol — copy-paste verifiers in four languages.

Every delivery carries:

X-Nembol-Signature: t=1784900412,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — 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 two v1= 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.

Last updated on July 28, 2026

Need help?

The API is in private beta — email us and a human replies, usually within one business day.