Genesys Cloud webhook signature verification failing on timestamp check

We’re trying to harden our webhook consumer in Berlin by verifying the X-Genesys-Signature header to prevent replay attacks. The docs say the signature is an HMAC-SHA256 of the payload combined with the timestamp. We’re using the Python hmac library to recreate it, but the comparison always fails.

Here’s the snippet we’re running:

import hmac
import hashlib

secret = 'our_webhook_secret_key'
body = request.data
timestamp = request.headers.get('X-Genesys-Timestamp')
header_sig = request.headers.get('X-Genesys-Signature')

# Docs imply it's just body + timestamp, but order matters
message = (body + timestamp).encode('utf-8')
calculated_sig = hmac.new(secret.encode('utf-8'), message, hashlib.sha256).hexdigest()

if calculated_sig != header_sig:
 print('Invalid signature')

The calculated_sig never matches header_sig. We’ve tried swapping the order of body and timestamp, adding a colon separator, and even URL-encoding the body, but nothing works. The timestamp header looks like a standard epoch string. We’re getting 200 OK responses from Genesys, so the delivery is fine, but our local validation is rejecting everything.

Is the payload raw bytes or the JSON string? The docs are vague on whether to include the BOM or newlines. We’re logging the raw request.data and it looks clean. Maybe the secret needs base64 decoding first? We’re stuck on this for two days. The config is fine, it’s just this auth check blocking us from processing the events. Any working examples in Python or Node would help. We need to know exactly what string is being hashed before the HMAC.

You’re missing the timestamp in the HMAC calculation. The signature isn’t just HMAC(secret, body). It’s HMAC(secret, timestamp + "." + body).

Check the X-Genesys-Timestamp header. You need to prepend it to the request body before signing. Also, ensure you’re using the raw bytes, not a string representation. Python’s hmac is strict about encoding.

Here’s the correct pattern:

import hmac
import hashlib
import time

def verify_signature(secret, body, timestamp_header):
 # Format: timestamp.body
 message = f"{timestamp_header}.{body}".encode('utf-8')
 signature = hmac.new(secret.encode('utf-8'), message, hashlib.sha256).hexdigest()
 
 # Check if timestamp is within 5 minutes (300 seconds)
 current_time = int(time.time())
 timestamp = int(timestamp_header)
 if abs(current_time - timestamp) > 300:
 raise ValueError("Timestamp expired or invalid")
 
 return hmac.compare_digest(signature, req.headers.get('X-Genesys-Signature'))

If it still fails, dump the raw body to a file and compare it byte-for-byte with what Genesys sent. Whitespace differences break the hash.