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().
Problem envelope
The RFC 9457 application/problem+json shape the SDK consumes, plus how it threads onto every SonnyLabsError as the raw problem attribute.
Scans
Conceptual documentation for the `/v1/scans` endpoint — how scans work, how the `tier` option selects a detector, and how to act on the returned decision.