Hey folks,
I’m trying to verify the X-Genesys-Signature header on incoming webhooks to prevent replay attacks. I’m using the Python SDK but I’m not sure how to reconstruct the expected HMAC signature from the request body. My code keeps failing the comparison.
import hmac
import hashlib
expected = hmac.new(secret.encode(), body.encode(), hashlib.sha256).hexdigest()
if expected != signature:
return 403
The signature always mismatches. Am I missing a step in the payload processing?
the issue is almost certainly how you’re handling the body encoding. genesys signs the raw request body as a string, but if your python middleware has already parsed it into a dictionary or if there’s any whitespace stripping happening, the hash won’t match.
also, make sure you’re using hmac.new correctly. the digestmod argument should be the hash function itself, not a string.
here is a working snippet using flask as an example, but the logic applies to fastapi or django too. you need to grab the raw data before any parsing happens.
import hmac
import hashlib
import os
def verify_genesys_signature(request_body: bytes, signature_header: str, secret: str) -> bool:
# genesys uses sha256
# ensure the body is treated exactly as it arrived
mac = hmac.new(
secret.encode('utf-8'),
msg=request_body,
digestmod=hashlib.sha256
)
expected_signature = mac.hexdigest()
# constant-time comparison to prevent timing attacks
return hmac.compare_digest(expected_signature, signature_header)
# usage in a route
# app = Flask(__name__)
# @app.route('/webhook', methods=['POST'])
# def handle_webhook():
# secret = os.environ['GENESYS_WEBHOOK_SECRET']
# sig = request.headers.get('X-Genesys-Signature')
#
# # critical: get the raw bytes, not json.loads()
# body = request.get_data()
#
# if not sig or not verify_genesys_signature(body, sig, secret):
# return "Unauthorized", 401
#
# # now you can safely parse json
# data = request.get_json()
# return "OK", 200
if you’re using fastapi, request.body() is async so you’ll need to await it. just don’t let the framework auto-decode it to utf-8 and strip newlines before you hash it. the signature includes every byte.
also double check that your secret in the app matches the one in the genesys webhook configuration exactly. trailing spaces are a common gotcha.