Verify the signature
Technical details and Node.js and Python code for verifying the X-Empath-Signature HMAC on every Empath webhook request.
Technical details of how to check that a request really came from Empath, with working code.
What is signed?
Empath computes an HMAC-SHA256 with your webhook's signing secret over this exact string:
{X-Empath-Timestamp}.{raw request body}
It sends the result as X-Empath-Signature: sha256=
.
- The timestamp is the decimal string from
X-Empath-Timestamp(seconds, no milliseconds). - The body is the exact bytes received. Verify before parsing, never against re-serialised JSON.
- Use a constant-time comparison.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyEmpathWebhook({ secret, timestamp, rawBody, signatureHeader }) {
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? '');
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
Python
import hashlib, hmac
def verify_empath_webhook(secret: str, timestamp: str, raw_body: bytes, signature_header: str) -> bool:
expected = "sha256=" + hmac.new(
secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature_header or "")
Good to know
- Replay protection is up to you. Empath doesn't enforce a time limit. We recommend rejecting requests whose timestamp is more than 5 minutes from your clock.
- Reject any request whose signature header doesn't start with
sha256=. - Every retry is signed again with a new timestamp, so a retry's signature differs from the first attempt's.
Still need help?
Reach out to your Partner Success Manager, or email us at support@empathmsp.com. We’re happy to help!
Related articles
- Request format: method, headers and timeouts
- Create a webhook: Review & test and the signing secret
- Build a receiver: the recommended pattern