Skip to main content

Webhook Signature

Zyphe signs webhook requests with HMAC-SHA256 when a signing secret is in force for the destination. Verify the signature before parsing or processing the payload.

Which secret signs a delivery

DestinationSigned with
Endpoint with secretMode: "ORGANIZATION"The organization's shared webhook secret
Endpoint with secretMode: "DEDICATED"That endpoint's own secret
Session webhookThe secret you supplied when creating the verification request

An organization that never configured a webhook secret keeps receiving unsigned deliveries — that state was preserved rather than "fixed" when endpoints were introduced. Configure a secret to start receiving X-Signature.

Each endpoint's secretHint (the last characters of the key in force) tells you which secret to verify against without ever disclosing it. A plaintext secret is returned exactly once, when it is created or rotated. See Webhook Endpoints.

Signature format

The X-Signature header contains a Unix timestamp and a hexadecimal signature:

t=<timestamp>,v0=<signature_hex>

Zyphe constructs the signed value from the timestamp, a dot, and the exact JSON request body:

signed_payload = "{timestamp}.{raw_json_payload}"

The webhook secret Zyphe generates is a hexadecimal string. Decode it into bytes, then calculate the hexadecimal HMAC-SHA256 digest of signed_payload. A secret you supplied yourself — for a session webhook — is used as you provided it.

Verification steps

  1. Read X-Signature and extract t and v0.
  2. Reject an invalid timestamp or one outside your accepted tolerance. A five-minute tolerance is a common starting point for replay protection.
  3. Read the raw, unmodified request body. Do not parse and re-serialize it before verification.
  4. Construct {t}.{raw_request_body}.
  5. Decode your hexadecimal webhook secret and calculate the HMAC-SHA256 digest.
  6. Compare the computed hexadecimal digest with v0 using a constant-time comparison.
  7. Parse and process the request only after all checks pass.

If the signature is missing, malformed, expired, or does not match, return an error and do not process the event. Keep the secret server-side and rotate it immediately if it is exposed.

Rotating

Rotating the shared organization secret re-keys every endpoint in ORGANIZATION mode at once; the response lists exactly which ones. New attempts are signed with the new secret immediately.

The previous secret is recorded as still acceptable until secretExpiresAt, one hour later — comfortably longer than the maximum retry window. Keep accepting it on your side until then, so a delivery signed just before the rotation still verifies when it arrives or is retried.

Rotating a dedicated secret affects only its own endpoint.

To roll over without a gap, accept both the old and the new secret during the grace window: compute both digests and accept a constant-time match against either.