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 data array 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. 1. Extract — take body.data as the list of job objects.
  2. 2. Sort by URL— sort the list ascending by each job's url field (string comparison).
  3. 3. Sort object keys— within every job object, sort keys alphabetically (equivalent to Python's sort_keys=True).
  4. 4. Serialize compactly — serialize with no spaces: separators=(",", ":") in Python, JSON.stringify without indent in TypeScript (default behavior).
  5. 5. Unicode handling — emit literal Unicode characters, not ASCII escapes. In Python, use ensure_ascii=False in json.dumps(). Do not allow JSON serialization to escape non-ASCII characters (e.g., em dashes, accents) as \uXXXX sequences — this produces different byte strings and breaks verification.
  6. 6. UTF-8 encode — encode the resulting string as UTF-8 bytes.
  7. 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_digest in Python, crypto.timingSafeEqual in Node/Bun). Never use == or === — timing attacks are real.
  • — Return 401 on mismatch. Do not process the payload.
  • — Return 401 if the header is missing entirely.

Common pitfalls

  • ASCII escaping mismatch: If your JSON serializer converts non-ASCII characters (em dashes, accents, etc.) to \uXXXX escape sequences, the signature will not match. Ensure literal Unicode output: Python requires ensure_ascii=False.
  • Incorrect input data: Signing the full request body {data: [...]} instead of just data alone. The signature is only over the jobs array, not the envelope.
  • Missing secret strip: If your signing secret has trailing whitespace (from .env files), 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-parser or an equivalent to parse once, then re-serialize with your canonicalization rules.