Signing & verification

Verify every webhook with the v1= HMAC scheme before you act on it.

The signature is v1= + hex HMAC-SHA256, keyed by your endpoint secret, over the bytes "{timestamp}.{delivery_id}." + raw_request_body. Verify with a constant time compare and reject anything older than 300 seconds. During a secret rotation grace window the header carries more than one comma-separated v1= candidate — accept the delivery if any candidate matches your secret.

Always verify against the raw request bytes, before any JSON parsing.

The SDK helpers implement the scheme (multi-candidate + tolerance) for you.

1# Python — see sdks/python/tests/test_webhooks.py for the tested calls.
2from drazill.webhooks import construct_webhook_event
3
4event = construct_webhook_event(raw_body, request.headers, endpoint_secret)
5print(event.type, event.data) # raises ValueError if the signature is invalid
1// TypeScript — see sdks/typescript/src/__tests__/webhooks.test.ts.
2import { constructWebhookEvent } from "drazill";
3
4const event = await constructWebhookEvent(rawBody, request.headers, endpointSecret);
5console.log(event.type, event.data); // throws if the signature is invalid

Raw recipe (no SDK)

This recipe is exercised verbatim by tests/test_webhooks.py::test_guide_raw_verification_recipe:

1import hashlib, hmac, time
2
3def verify(raw_body: bytes, headers: dict, secret: str, tolerance: int = 300) -> bool:
4 timestamp = headers["Drazill-Webhook-Timestamp"]
5 delivery_id = headers["Drazill-Webhook-Id"]
6 if abs(int(time.time()) - int(timestamp)) > tolerance:
7 return False
8 signed = f"{timestamp}.{delivery_id}.".encode() + raw_body
9 expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
10 # A rotation grace window sends multiple comma-separated candidates.
11 return any(
12 hmac.compare_digest(expected, candidate.strip())
13 for candidate in headers["Drazill-Webhook-Signature"].split(",")
14 )

Respond 2xx once you have durably accepted the event. Any non-2xx (or a timeout) is a failed attempt and will be retried.

Verify against the raw bytes before parsing JSON, and use a constant-time compare (hmac.compare_digest) — never ==. A webhook is a notification: re-read authoritative state from the REST API before moving money.