CXone EventBridge webhook signature verification failing on replay

Getting Signature verification failed when trying to validate incoming EventBridge events in our Node.js consumer. We’ve set up the webhook in CXone to push routing.queue.conversation.created events, and the payload looks fine, but the crypto check is failing every single time.

Here’s the verification logic we’re using:

const crypto = require('crypto');

function verifyWebhook(payload, signature, secret) {
 const hmac = crypto.createHmac('sha256', secret);
 hmac.update(payload);
 const digest = hmac.digest('hex');
 return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}

We’re grabbing the X-CXone-Signature header from the request and comparing it against the HMAC of the raw body. The secret is the one generated in the CXone admin console under the webhook configuration.

The weird part is that if I log the digest and the incoming signature, they look identical at a glance, but timingSafeEqual returns false. I’ve tried removing whitespace, converting to lowercase, even just doing a simple === string compare, but nothing sticks.

Also, we’re seeing these events arrive in bursts, sometimes milliseconds apart. Is CXone sending the timestamp as part of the payload that needs to be included in the HMAC calculation? The docs are pretty vague on exactly which fields get signed. I assumed it was just the raw JSON body, but maybe it’s the body plus the X-CXone-Timestamp header?

Here’s a sample payload header we’re receiving:

X-CXone-Signature: a1b2c3d4...
X-CXone-Timestamp: 1678886400

And the body is standard JSON:

{
 "event_id": "evt_12345",
 "type": "routing.queue.conversation.created",
 "payload": { ... }
}

If I skip the verification, the logic works fine, but we can’t leave it open like that. Anyone else hit this with EventBridge? Or is this specific to the CXone webhook signing implementation?