Verifying X-Genesys-Cloud-Signature in Node.js Express handler

Setting up an Express endpoint to catch /api/v2/interaction/case-workflow webhooks. Trying to verify the signature header before processing payloads to stop replay attacks. The platform docs mention HMAC-SHA256 with the client secret, but the hash never matches.

const crypto = require('crypto');
const sig = req.headers['x-genesys-cloud-signature'];
const ts = req.headers['x-genesys-cloud-timestamp'];
const raw = JSON.stringify(req.body);
const expected = crypto.createHmac('sha256', process.env.GENESYS_SECRET)
 .update(`${ts}.${raw}`)
 .digest('hex');

It’s failing on every request. We’ve double-checked the secret in the integration settings. Maybe the payload needs to be raw instead of parsed JSON? Express body-parser might be mucking with the stringification order. The timestamp window can’t be that tight for Tokyo to US-East latency. Anyone got a working verification snippet for Express? Logs show the events hitting the queue but the signature check kills the callback every time. Not sure why the HMAC keeps drifting.

Are you including the timestamp in the payload string before signing? The docs state: “The signature is an HMAC-SHA256 hash of the raw request body concatenated with the timestamp.” If you’re just signing the JSON, it’ll always fail. You need to append the timestamp from the header to the raw string first. It’s a common trip-up when moving from basic auth to webhook verification.

Here’s how the verification should look in Express. Make sure you’re using the raw body, not the parsed object, because stringify changes whitespace.

const crypto = require('crypto');

function verifySignature(req, secret) {
 const ts = req.headers['x-genesys-cloud-timestamp'];
 const sig = req.headers['x-genesys-cloud-signature'];
 const rawBody = req.rawBody; // Ensure express body-parser is configured to retain rawBody

 const stringToSign = `${rawBody}${ts}`;
 const expected = crypto
 .createHmac('sha256', secret)
 .update(stringToSign)
 .digest('hex');

 return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}