CXone DFO webhook signature verification failing with HMAC SHA256

Setting up a receiver for DFO webhooks. Need to verify the signature in the X-CXone-Signature header to stop replay attacks. The docs say it’s HMAC SHA256 using the shared secret, but my verification always fails.

Here’s the Node.js snippet I’m using:

const crypto = require('crypto');

app.post('/webhook', (req, res) => {
 const signature = req.headers['x-cxone-signature'];
 const payload = JSON.stringify(req.body);
 const secret = process.env.WEBHOOK_SECRET;

 const hmac = crypto.createHmac('sha256', secret);
 hmac.update(payload);
 const digest = hmac.digest('hex');

 if (signature !== digest) {
 return res.status(401).send('Invalid signature');
 }
 
 res.status(200).send('OK');
});

The payload string matches exactly what I see in the request body. The secret is correct. I’ve tried using the raw buffer instead of hex, same result. Is CXone including the content-type or timestamp in the signed string? Nothing in the DFO API docs clarifies the exact string-to-sign format.