Zendesk vs Sume webhook signatures: base64 no dot vs hex with a dot

Zendesk signs base64(HMAC-SHA256(timestamp + body)) with no separator; Sume signs timestamp.body as sume-v1 hex. One verifier cannot check both.

4 min readSume
All posts

If your service receives both Zendesk and Sume webhooks, you need two verifiers. Zendesk signs base64(HMACSHA256(TIMESTAMP + BODY)) with nothing between the timestamp and the body. Sume signs <timestamp>.<raw_body>, with a dot, and sends the digest as hex behind a sume-v1= prefix. Copying one check to the other fails every request.

Side by side

Both vendors use HMAC-SHA256 over a timestamp and the raw body, which makes them look alike. The differences are in the details.

Zendesk and Sume webhook signature formats (vendor docs, read 2026-10-05)
DetailZendeskSume
Timestamp headerX-Zendesk-Webhook-Signature-Timestampx-sume-webhook-timestamp
Signature headerX-Zendesk-Webhook-Signaturex-sume-webhook-signature
Signed stringTIMESTAMP + BODY, no separator<timestamp>.<raw_body>, with a dot
EncodingBase64Hex, prefixed sume-v1=
During rotationOne signatureComma-separated entries, newest first (24 hour overlap)

Two functions

Keep them apart and name them for the vendor. The Sume SDK has verifyWebhook({ body, headers, secret }) for its side; the Zendesk side is a few lines of standard library code.

import base64, hashlib, hmac

def zendesk_ok(raw: bytes, ts: str, sig: str, secret: str) -> bool:
    if not secret:
        raise RuntimeError("empty secret")
    mac = hmac.new(secret.encode(), ts.encode() + raw, hashlib.sha256).digest()
    return hmac.compare_digest(base64.b64encode(mac).decode(), sig)

def sume_ok(raw: bytes, ts: str, header: str, secret: str) -> bool:
    if not secret:
        raise RuntimeError("empty secret")
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw, hashlib.sha256)
    want = "sume-v1=" + mac.hexdigest()
    return any(hmac.compare_digest(want, p.strip()) for p in header.split(","))

The rotation detail

During a Sume secret rotation the header carries more than one signature, newest first, for 24 hours. The any(...) loop above accepts the request if any listed entry matches your secret, so a delivery signed during the overlap still verifies. Read the current value from /dashboard/webhooks or GET /v1/webhooks/signing-secret. Zendesk's page describes a single signature, so no such loop is needed there.

Limits

Parse the body only after the check passes, and compare the raw bytes you received, not a re-serialized object. A proxy that rewrites whitespace will break both schemes. Sume's timestamp is in epoch seconds and its helper checks a replay window of 300 seconds by default (toleranceSeconds); the example above leaves that check out for brevity, so add it before production.

Before you ship

  • Keep one verifier per vendor; do not share code between them.
  • Read the raw body bytes before any JSON parsing.
  • Reject an empty secret at startup.
  • Test with a real signed sample from each side.
  • Compare with hmac.compare_digest or the platform equivalent, never with ==
  • Handle a missing header as a rejection, not as an exception that returns 500.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume