Skip to main content

Signature Verification

Every webhook request is signed with your webhook secret. Verify the signature to confirm the request is genuine.

Headers​

HeaderValue
X-SalesLobe-Webhook-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")> (recommended)
X-SalesLobe-TimestampThe same t, for convenience
X-SalesLobe-Event-IdStable across retries of the same event: use it to dedupe
X-SalesLobe-Delivery-IdOne per delivery attempt to your endpoint
X-SalesLobe-EventThe event type
X-SalesLobe-Signaturesha256=<hex HMAC-SHA256(secret, raw body)> (legacy, still sent)

Prefer X-SalesLobe-Webhook-Signature: because the timestamp is signed too, you can reject replays. Reject requests where t is more than 5 minutes from your clock, and store X-SalesLobe-Event-Id to ignore duplicates. The legacy X-SalesLobe-Signature header keeps working for existing integrations.

Use the raw request body

The signature is computed over the exact bytes sent as the HTTP body. Never use JSON.stringify(parsed) or json.dumps(parsed) for verification — the key order of a re-serialized object is not guaranteed and will produce different bytes, causing the signature to fail. Always capture the raw body before parsing.

const crypto = require('crypto');

function verifyWebhookV1(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries((header || '').split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.`).update(rawBody) // raw bytes, NOT JSON.stringify()
.digest('hex');
return parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}

// if (!verifyWebhookV1(req.rawBody, req.get('X-SalesLobe-Webhook-Signature'), SECRET))
// return res.status(401).end();

Legacy signature​

const crypto = require('crypto');

// Express: capture raw bytes via verify callback
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));

function verifyWebhook(rawBody, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody) // ← raw bytes, NOT JSON.stringify()
.digest('hex');
return signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// In your route handler:
// if (!verifyWebhook(req.rawBody, req.get('X-SalesLobe-Signature'), SECRET))
// return res.status(401).end();