Appearance
Verifying Webhook Signatures ​
Anyone can POST to your endpoint claiming to be Gnosari. Verifying the signature on every delivery is how you confirm a request genuinely came from Gnosari and that the body was not altered in transit.
Scheme ​
Gnosari signs deliveries using Standard Webhooks, an open, published HMAC signing scheme. Any Standard Webhooks-compatible library, in any language, verifies a Gnosari delivery correctly.
Headers ​
Every delivery carries three headers:
| Header | Description |
|---|---|
webhook-id | The delivery identity. The SAME value across every attempt of one delivery, including retries |
webhook-timestamp | Unix seconds, when the request was signed |
webhook-signature | v1,<base64 HMAC>. Space-separated if more than one signature is ever present |
Signed Content ​
The signature covers exactly this string, joined with .:
{webhook-id}.{webhook-timestamp}.{raw request body}Verify against the raw bytes of the request body as received, not a re-serialized version of it. Re-serializing (even with no semantic change, like different key ordering) changes the bytes, so the signature will no longer match.
Where To Find The Secret ​
Each destination has its own signing secret. Read it once from GET /api/v1/webhook-subscriptions/{id}/secret and store it securely; it is not shown anywhere else.
Worked Example (Python) ​
python
import base64
import hashlib
import hmac
def verify_gnosari_signature(secret: str, webhook_id: str, timestamp: str, signature_header: str, raw_body: bytes) -> bool:
"""Verify a Gnosari webhook delivery using stdlib only.
Args:
secret: The destination's whsec_... signing secret.
webhook_id: The `webhook-id` header value.
timestamp: The `webhook-timestamp` header value.
signature_header: The `webhook-signature` header value.
raw_body: The exact, unmodified request body bytes.
Returns:
True if at least one v1 signature in the header matches.
"""
secret_body = secret[len("whsec_"):] if secret.startswith("whsec_") else secret
key = base64.b64decode(secret_body)
signed_content = f"{webhook_id}.{timestamp}.".encode() + raw_body
expected_signature = hmac.new(key, signed_content, hashlib.sha256).digest()
expected_b64 = base64.b64encode(expected_signature).decode()
for entry in signature_header.split(" "):
version, _, value = entry.partition(",")
if version == "v1" and hmac.compare_digest(expected_b64, value):
return True
return FalseUsage in a request handler:
python
secret = "whsec_..." # from GET /webhook-subscriptions/{id}/secret
is_valid = verify_gnosari_signature(
secret,
request.headers["webhook-id"],
request.headers["webhook-timestamp"],
request.headers["webhook-signature"],
request.body, # raw bytes, not request.json()
)
if not is_valid:
return Response(status_code=401)Use the official library instead, when available. The standardwebhooks library (available for multiple languages) implements the same scheme end to end and handles edge cases like timestamp tolerance windows for you:
python
from standardwebhooks import Webhook
wh = Webhook("whsec_...")
wh.verify(raw_body, {
"webhook-id": request.headers["webhook-id"],
"webhook-timestamp": request.headers["webhook-timestamp"],
"webhook-signature": request.headers["webhook-signature"],
}) # raises on a bad signatureWhat Tampering Looks Like ​
Altering even one character of the body changes the computed HMAC, so a modified body never verifies against the original signature. A request forged without knowledge of the destination's secret cannot produce a signature that verifies at all, since the HMAC key is the secret itself.