Node.js Lambda: Genesys Cloud webhook signature verification failing with HMAC SHA256

Building a Lambda consumer for Genesys Cloud webhooks. Specifically handling conversation:created events. The payload arrives correctly in the Lambda handler, but the signature verification is failing. I’m using the crypto module in Node.js 18 to validate the X-GC-Signature header against the payload body.

The logic looks standard. Grab the header, strip the t= prefix to get the timestamp, then compute the HMAC SHA256 using the shared secret. The issue is that the computed hash never matches the provided signature. I’ve checked the secret key multiple times. It’s correct.

Here’s the verification logic:

const crypto = require('crypto');

exports.handler = async (event) => {
 const body = JSON.stringify(event.body);
 const signatureHeader = event.headers['x-gc-signature'];
 const secret = process.env.GC_WEBHOOK_SECRET;

 // Parse timestamp from signature
 const timestamp = signatureHeader.split('=')[1];
 const expectedSignature = crypto
 .createHmac('sha256', secret)
 .update(timestamp + body)
 .digest('hex');

 console.log('Expected:', expectedSignature);
 console.log('Provided:', signatureHeader);

 if (expectedSignature !== signatureHeader) {
 return {
 statusCode: 401,
 body: JSON.stringify({ error: 'Signature mismatch' })
 };
 }

 return {
 statusCode: 200,
 body: JSON.stringify({ message: 'Verified' })
 };
};

The logs show the hashes are completely different. The length is the same, obviously, but the values don’t align. I’m wondering if the body parsing in the Lambda event object is altering the payload structure before I stringify it. Or maybe the signature includes the raw POST body bytes, not the JSON string representation.

I’ve tried using event.body directly and also JSON.parse(event.body) then stringifying it back. No luck. The documentation says “HMAC-SHA256 of the timestamp concatenated with the request body”. It’s vague on what constitutes the “request body” in this context. Raw bytes? UTF-8 string? Pretty-printed JSON?

Any insights on how Genesys Cloud constructs the signature payload for verification? I need to know the exact string input for the HMAC function. Currently guessing it’s the raw HTTP body, but Lambda might be decoding it differently.