Verifying Genesys Cloud webhook signature in Python failing on replay check

Hey everyone,

I’m trying to secure our internal WFM dashboard that receives Genesys Cloud event webhooks. We’ve been getting hit by some weird replay attacks where old events are being resent and messing up our adherence calculations. I need to verify the signature header to make sure the payload is fresh and hasn’t been tampered with.

I’m using Python with Flask to handle the incoming POST requests. The documentation mentions checking the X-Genesys-Signature header, but I’m not sure if I’m constructing the payload string correctly for the HMAC calculation.

Here is the relevant snippet from my route handler:

import hmac
import hashlib
import os

@app.route('/webhook/event', methods=['POST'])
def handle_webhook():
 signature = request.headers.get('X-Genesys-Signature')
 timestamp = request.headers.get('X-Genesys-Timestamp')
 body = request.get_data(as_text=True)
 
 # My secret from the Genesys Cloud webhook config
 secret = os.environ.get('GENESYS_WEBHOOK_SECRET')
 
 # Constructing the string to sign
 # Not sure if I should include the timestamp or just the body?
 string_to_sign = timestamp + body
 
 expected_signature = hmac.new(
 secret.encode('utf-8'),
 string_to_sign.encode('utf-8'),
 hashlib.sha256
 ).hexdigest()
 
 if not hmac.compare_digest(signature, expected_signature):
 return "Signature mismatch", 401
 
 return "OK", 200

The issue is that hmac.compare_digest is always returning False. I’ve double-checked the secret key and it matches what’s in the Genesys Cloud integration settings. The timestamp header seems to be present, but I’m guessing I might be formatting the string_to_sign wrong. Should I be hashing just the body? Or is there a specific format for combining the timestamp and payload that I’m missing?

Any help would be appreciated. I don’t want to leave this endpoint open to replay issues while I figure this out.

Are you using the standard HMAC-SHA256 algorithm for the signature check? The docs can be a bit vague on the exact byte ordering. Here’s a quick snippet that usually works for Flask. Make sure you’re using the raw body, not the parsed JSON, for the hash calculation. That’s where most people trip up.

import hmac
import hashlib

def verify_signature(secret, raw_body, signature_header):
 # Remove 'sha256=' prefix if present
 sig = signature_header.replace('sha256=', '')
 expected = hmac.new(
 secret.encode('utf-8'),
 raw_body,
 hashlib.sha256
 ).hexdigest()
 
 return hmac.compare_digest(expected, sig)

Also, check the timestamp in the header. If it’s more than five minutes old, drop it. Replay attacks usually rely on stale data. You don’t need complex logic here. Just verify the hash and the time window. If the hash matches but the time is off, log it and ignore the payload. It’s messy but effective.