Skip to content

Verifying webhooks

Each delivery includes three headers:

X-Webhook-Id: <uuid>
X-Webhook-Timestamp: 1748254473
X-Webhook-Signature: v1=<hex>

The signature is built as:

HMAC_SHA256(
key = shared_secret,
msg = timestamp + "." + raw_body
).hex_digest()

It is then prefixed with v1= so future schemes can coexist. The shared secret is the value you sent as key when creating the webhook; Quiddly stores it encrypted at rest and never echoes it back.

import hmac
import hashlib
import time
def verify(secret: str, body: bytes, timestamp: str, signature: str) -> bool:
if abs(time.time() - int(timestamp)) > 5 * 60:
return False
mac = hmac.new(
key=secret.encode("utf-8"),
msg=timestamp.encode("utf-8") + b"." + body,
digestmod=hashlib.sha256,
)
expected = f"v1={mac.hexdigest()}"
return hmac.compare_digest(expected, signature)

A 5-minute clock-skew tolerance is recommended.

Update the webhook with a new key:

Terminal window
curl -X PUT "https://$HOST/webhook/$WH_ID" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-d '{ "key": "<new-shared-secret>" }'

Quiddly re-encrypts the new secret and signs subsequent deliveries with it. There is no overlap window today; coordinate the cutover with your receiver.

If your receiver expects a static shared secret in a header rather than HMAC, set useHeaderSecret: true and keyParam: "<header-name>" on the subscription. Quiddly will then attach the secret as that header instead of signing the body. HMAC remains the default and recommended scheme.