Genesys Cloud Webhook Signature Verification Failing

GET /api/v2/webhooks/instances returns the config but the actual payload verification in my Node.js consumer is failing. The signature header x-genesys-signature doesn’t match what I’m computing. I’m using the crypto module in Node. Here’s the snippet:

const crypto = require('crypto');
const secret = process.env.GENESYS_WEBHOOK_SECRET;
const payload = req.body;

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

if (signature !== req.headers['x-genesys-signature']) {
 return res.status(401).send('Invalid signature');
}

The docs say it’s HMAC-SHA256. I’m logging both values and they look completely different. Not sure if I need to URL encode the payload or if the secret key I pulled from the admin UI is wrong. The webhook is set up for conversation:created events. Is there a specific format for the secret? It’s not base64 encoded in the UI. Trying to prevent replay attacks so I can’t just ignore the header. Any idea why the hex output differs?

You’re computing the hash on req.body which is likely a parsed object by the time your middleware hits it. The signature is generated against the raw request body string, not the JSON object. If you’re using Express, body-parser or express.json() consumes the stream and converts it to an object before you can read the raw bytes. You need to capture the raw body first.

Here’s how to fix it in Node.js. Stop using req.body directly for the HMAC. Buffer the raw input stream instead.

const crypto = require('crypto');

// Middleware to capture raw body
function captureRawBody(req, res, next) {
 let body = '';
 req.on('data', chunk => {
 body += chunk.toString(); // must be a string, not Buffer
 });
 req.on('end', () => {
 req.rawBody = body;
 next();
 });
}

app.use(captureRawBody);

// Your verification logic
const secret = process.env.GENESYS_WEBHOOK_SECRET;
const expectedSignature = crypto
 .createHmac('sha256', secret)
 .update(req.rawBody) // Use the raw string here
 .digest('hex');

const receivedSignature = req.headers['x-genesys-signature'];

if (receivedSignature !== expectedSignature) {
 return res.status(401).send('Invalid signature');
}

Make sure GENESYS_WEBHOOK_SECRET matches the secret configured in the webhook definition under /api/v2/webhooks/instances/{webhookId}. Also check that you aren’t trimming whitespace from the raw body. Genesys sends the exact JSON string it generated. If there’s a newline at the end or missing spaces, the hash won’t match. It’s a byte-for-byte comparison.

I’ve seen this fail silently when devs log req.body for debugging and accidentally mutate it. Keep the raw string untouched. If it’s still failing, dump both receivedSignature and expectedSignature to a log file and compare character by character. Sometimes it’s a case sensitivity issue with the header name. Genesys sends x-genesys-signature in lowercase. Express might normalize it to uppercase depending on your setup. Use req.headers['x-genesys-signature'] explicitly.