/** * Simplifi webhook verification (Node.js / Express). * * Verify the X-Simplifi-Signature header against the RAW request body * before parsing any JSON. Use express.raw() (not express.json()) on the * webhook route so the exact bytes are available. */ const crypto = require('crypto'); const express = require('express'); const app = express(); const SIGNING_SECRET = process.env.SIMPLIFI_WEBHOOK_SECRET; // your endpoint's signing_secret app.post('/webhooks/simplifi', express.raw({ type: 'application/json' }), (req, res) => { const header = req.get('X-Simplifi-Signature') || ''; // "sha256={hex}" if (!header.startsWith('sha256=')) { return res.status(400).end(); } const expected = crypto .createHmac('sha256', SIGNING_SECRET) .update(req.body) // raw Buffer .digest('hex'); const received = header.slice('sha256='.length); const a = Buffer.from(expected, 'hex'); const b = Buffer.from(received, 'hex'); if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) { return res.status(401).end(); } const event = JSON.parse(req.body.toString('utf8')); // Deduplicate on event.id - retries and resends keep the same delivery id. // Respond 2xx fast; do real work asynchronously (queue, etc.). res.status(200).send('ok'); // switch (event.type) { // case 'document.signer.signed': ...; break; // case 'document.completed': ...; break; // case 'webhook.test': ...; break; // } }); app.listen(3000);