Hey folks,
We’re trying to lock down our webhook consumers to prevent replay attacks. The docs suggest verifying the X-Genesys-Signature header against the request body. I’ve got a basic Node.js Express handler set up, but the calculated hash never matches the incoming signature. It’s driving me nuts because the payload seems fine, and the timestamp is valid.
Here’s the relevant snippet:
app.post('/webhook/gen-events', (req, res) => {
const signature = req.headers['x-genesys-signature'];
const timestamp = req.headers['x-genesys-timestamp'];
const body = req.body;
// Using the shared secret from the integration config
const secret = process.env.GENESYS_WEBHOOK_SECRET;
// Constructing the string to sign
const payload = JSON.stringify(body);
const stringToSign = `${timestamp}.${payload}`;
const calculatedHash = crypto
.createHmac('sha256', secret)
.update(stringToSign)
.digest('hex');
console.log('Incoming:', signature);
console.log('Calculated:', calculatedHash);
if (signature !== calculatedHash) {
return res.status(401).send('Invalid Signature');
}
res.status(200).send('OK');
});
The logs show the hashes are completely different. I’ve double-checked the secret key, and it’s definitely the one from the Webhook configuration in Genesys Cloud. I’m using req.body directly from Express, so I assumed it was the raw JSON. But wait, does Express parse the body before I get it? If the body is already a JS object, JSON.stringify might reorder keys or change whitespace, which would break the hash.
I tried switching to req.rawBody but that requires a middleware setup I haven’t done yet. Is there a specific way Genesys expects the string to be constructed? Or am I missing something obvious about how the signature is generated? It feels like I’m close but missing a small detail about the payload format. Any pointers?