Skip to content

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:

HeaderDescription
webhook-idThe delivery identity. The SAME value across every attempt of one delivery, including retries
webhook-timestampUnix seconds, when the request was signed
webhook-signaturev1,<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 False

Usage 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 signature

What 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.