signature verification
Python
Drop either of these into your Python HTTP framework. Both reproduce the exact canonicalization used by the Freshbatch publisher worker. Requires Python 3.8+, no extra dependencies beyond FastAPI.
FastAPI handler
A complete, ready-to-run endpoint. Replace WEBHOOK_SECRET with your signing key from Dashboard Settings.
import hmac
import hashlib
import json
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = "replace-with-your-sign-key"
@app.post("/webhook")
async def receive_webhook(
request: Request,
webhook_signature: str = Header(..., alias="webhook-signature"),
):
body = await request.json()
jobs = body.get("data", [])
# Canonicalize: sort by url, sort object keys, compact separators
canonical = json.dumps(
sorted(jobs, key=lambda job: job["url"]),
sort_keys=True,
separators=(",", ":"),
)
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
canonical.encode("utf-8"),
hashlib.sha256,
).hexdigest()
# Constant-time compare — never use ==
if not hmac.compare_digest(expected, webhook_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
# Process jobs...
return {"ok": True, "received": len(jobs)}Standalone helper
Framework-agnostic function. Pass in the parsed job list, your secret, and the header value. Returns a boolean.
import hmac
import hashlib
import json
def verify_freshbatch_signature(
jobs: list[dict],
secret: str,
received: str,
) -> bool:
"""
Returns True if the received signature matches.
Uses constant-time comparison (hmac.compare_digest).
"""
canonical = json.dumps(
sorted(jobs, key=lambda j: j["url"]),
sort_keys=True,
separators=(",", ":"),
)
expected = hmac.new(
secret.encode("utf-8"),
canonical.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, received)See Signature Verification overview for a step-by-step explanation of the canonicalization rules. For a TypeScript version, see TypeScript