Genesys Cloud webhook signature verification failing on replay protection

We’ve set up an internal service to ingest .queue.member.assigned events from Genesys Cloud via a custom webhook endpoint. The goal is to trigger a downstream notification, but we need to ensure the requests are legit. I’m trying to implement signature verification to stop replay attacks, following the docs on signing requests. The problem is the signature check is always failing on the first request, even though the payload looks correct.

Here’s the Node.js snippet handling the verification. We’re using the crypto module to generate the HMAC SHA256 hash. The secret comes from the environment variable set during the webhook creation in the admin UI.

const crypto = require('crypto');

app.post('/webhook/genesys', (req, res) => {
 const signature = req.headers['x-genesys-signature'];
 const timestamp = req.headers['x-genesys-timestamp'];
 const body = JSON.stringify(req.body);

 const expectedSignature = crypto
 .createHmac('sha256', process.env.GENESYS_WEBHOOK_SECRET)
 .update(`${timestamp}${body}`)
 .digest('hex');

 if (signature !== expectedSignature) {
 console.log('Signature mismatch');
 res.status(401).send('Unauthorized');
 return;
 }
 
 res.status(200).send('OK');
});

The x-genesys-signature header arrives, but the hash never matches. I’ve tried changing the update string to just the body, or body plus timestamp, but nothing works. The timestamp header is there, formatted as a Unix epoch string. Am I missing a step in the concatenation? The docs are vague on the exact input string format for the HMAC. Also, the timestamp seems to drift a bit when I log it, which might be causing issues if there’s a time window check I’m unaware of. No error codes from Genesys, just my code rejecting it.

Are you encoding the raw body or the parsed JSON object? That’s the most common trap. The signature is calculated against the exact raw HTTP body, including whitespace and newlines. If you run JSON.stringify() on the parsed object, you might lose the original formatting or change the order of keys, which breaks the hash.

Here’s how to handle it in Express. You need to capture the raw body before parsing it.

app.use(express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
 const rawBody = req.body.toString('utf-8');
 const signature = req.headers['x-genesys-signature'];
 
 // Verify signature using your secret and the rawBody string
 const isValid = verifySignature(rawBody, signature, secret);
 
 if (!isValid) return res.status(401).send('Invalid signature');
 
 const payload = JSON.parse(rawBody);
 // process payload...
 res.status(200).send();
});

Also check the timestamp header. Genesys rejects requests older than 5 minutes by default. If your server clock is off, or there’s significant network latency, it’ll fail. Make sure your server time is synced.