SDK ReferencePython
verify_webhook
Constant-time HMAC-SHA256 verification for inbound Sonny Labs webhook deliveries, plus the tolerance and clock-injection knobs.
verify_webhook validates the Sonny-Signature header on an inbound
webhook delivery. The backend signs each delivery with
Sonny-Signature: t=<unix>,v1=<hex> where <hex> is
HMAC-SHA256(secret, "{t}.{body}"). Receivers must verify the MAC
and reject deliveries whose timestamp falls outside a 5-minute
replay window — the helper enforces both in one call.
Signature
from sonnylabs import verify_webhook
def verify_webhook(
body: bytes,
signature_header: str,
secret: str,
*,
tolerance_s: int = 300,
now: float | None = None,
) -> bool: ...Returns True if both the HMAC and the timestamp window check
pass; False otherwise. Never raises on a malformed header — that's
just an invalid signature.
Arguments
Prop
Type
Example: FastAPI
from fastapi import FastAPI, Header, HTTPException, Request
from sonnylabs import verify_webhook
app = FastAPI()
@app.post("/webhooks/sonny")
async def receive(
request: Request,
sonny_signature: str = Header(...),
):
# Read the *raw* bytes — re-serialising the JSON breaks HMAC
# verification because the digest is over the exact bytes the
# server signed.
body = await request.body()
ok = verify_webhook(body, sonny_signature, settings.SONNY_WEBHOOK_SECRET)
if not ok:
raise HTTPException(status_code=400, detail="invalid signature")
# ... process the event
return {"ok": True}Notes
- Pass
bodyas rawbytes— never a parsed-and-re-serialised representation. Different JSON serialisers reorder keys / re-quote strings differently and HMAC is sensitive to every byte. - Multiple
v1=entries are tolerated in the header — the SDK checks every candidate so secret rotation (server signing with both old and new secrets) keeps working without coupling the receiver to which secret matched. - The
nowkeyword argument exists for tests; production callers should leave it unset so the helper reads fromtime.time().