signature verification
Verify Webhook-Signature
Every delivery includes a webhook-signature header containing an HMAC-SHA256 hex digest. Verify it before processing the payload to confirm the request came from Freshbatch and was not tampered with.
Algorithm
- — Algorithm: HMAC-SHA256
- — Header name:
webhook-signature - — Encoding: lowercase hex digest
- — What is signed: the
dataarray from the request body — NOT the full body
Canonicalization rules
To reproduce the exact byte string that was signed, follow these steps in order. Deviation in any step will produce a different digest and cause verification to fail.
- 1. Extract — take
body.dataas the list of job objects. - 2. Sort by URL— sort the list ascending by each job's
urlfield (string comparison). - 3. Sort object keys— within every job object, sort keys alphabetically (equivalent to Python's
sort_keys=True). - 4. Serialize compactly — serialize with no spaces:
separators=(",", ":")in Python,JSON.stringifywithout indent in TypeScript (default behavior). - 5. Unicode handling — emit literal Unicode characters, not ASCII escapes. In Python, use
ensure_ascii=Falseinjson.dumps(). Do not allow JSON serialization to escape non-ASCII characters (e.g., em dashes, accents) as\uXXXXsequences — this produces different byte strings and breaks verification. - 6. UTF-8 encode — encode the resulting string as UTF-8 bytes.
- 7. HMAC-SHA256 — compute HMAC-SHA256 of those bytes using your signing secret (also UTF-8 encoded). Take the hex digest.
Verification rules
- — Use constant-time comparison (
hmac.compare_digestin Python,crypto.timingSafeEqualin Node/Bun). Never use==or===— timing attacks are real. - — Return
401on mismatch. Do not process the payload. - — Return
401if the header is missing entirely.
Common pitfalls
- — ASCII escaping mismatch: If your JSON serializer converts non-ASCII characters (em dashes, accents, etc.) to
\uXXXXescape sequences, the signature will not match. Ensure literal Unicode output: Python requiresensure_ascii=False. - — Incorrect input data: Signing the full request body
{data: [...]}instead of justdataalone. The signature is only over the jobs array, not the envelope. - — Missing secret strip: If your signing secret has trailing whitespace (from
.envfiles), it will not match. Load and trim carefully. - — Middleware body mutation: Express middleware that parses the body and then re-stringifies can reorder keys or change spacing, breaking the canonical form. Always use
body-parseror an equivalent to parse once, then re-serialize with your canonicalization rules.