Zendesk webhook signature verification failing? Check these 5 causes, in order

Zendesk's webhook signing scheme is short, but it breaks three habits you've built verifying GitHub- and Stripe-style webhooks: the timestamp is an ISO-8601 string (not unix seconds), the output is base64 (not hex), and — the big one — the secret that signs test deliveries is not the secret that signs production deliveries. Here's the exact algorithm, then the failure causes in the order to check them.

What Zendesk actually sends

X-Zendesk-Webhook-Signature: Dvp/wwSV2zdkL4q1NOvdohPaR8iFwnV36MU60vLrUT0=
X-Zendesk-Webhook-Signature-Timestamp: 2026-08-30T14:12:08Z
X-Zendesk-Webhook-Id: 01F1KRFQ6BG29CNWFR60NK5FNY
X-Zendesk-Webhook-Invocation-Id: 8350205582

The signature is computed as:

signature = base64( HMAC_SHA256( key = signing_secret,
                                 message = timestamp + raw_body ) )

where timestamp is the ISO-8601 string exactly as it appears in the header — e.g. 2026-08-30T14:12:08Z — concatenated directly with the raw body bytes, no separator.

Cause 1: you pinned the static test secret (or verified a test delivery with the real one)

This is the failure unique to Zendesk. While you are still creating a webhook in Admin Center, the "Test webhook" deliveries are signed with a documented static test secret:

dGhpc19zZWNyZXRfaXNfZm9yX3Rlc3Rpbmdfb25seQ==

(base64 for this_secret_is_for_testing_only). The real per-webhook signing secret only exists after the webhook is created — Admin Center → your webhook → Reveal secret, or the Show Webhook Signing Secret API. So both directions fail:

Cause 2: unix-timestamp assumptions

Stripe/Paddle/WorkOS muscle memory says "parse t as a number". Zendesk's timestamp is a string like 2026-08-30T14:12:08Z and it goes into the HMAC as that string. Converting it to epoch seconds, reformatting it, or inserting a ./: separator between timestamp and body all produce a different digest.

Cause 3: decoding the secret

The signing secret looks base64-encoded, and Zendesk's own Node sample feeds it into the HMAC verbatim — never decoded. If your code base64-decodes the secret before keying the HMAC, nothing will ever match.

Cause 4: re-serialized body

The classic. The HMAC covers the raw request bytes; any framework that parses JSON and re-stringifies it before your verifier runs (key order, whitespace, unicode escapes, big-number precision — the sequence.id in Zendesk payloads is a good canary) breaks verification. Verify against the raw body.

Cause 5: comparing hex to base64

Most HMAC examples output hex; Zendesk's header is base64. hexdigest() compared against Dvp/wwSV…= fails every time even when the digest bytes are identical. Encode base64, compare constant-time.

Check a real delivery in one minute

Point the webhook (or a trigger) at a Hookden bin, set the bin's signature scheme to Zendesk and paste your secret — every capture gets a live ✓/✗ badge computed from the exact bytes Zendesk sent, which tells you immediately whether the problem is your key material or your code's body handling. Or paste body + headers + secret into the client-side signature debugger (nothing leaves your browser). A realistic capture to poke at: Zendesk ticket.created payload example — its signature verifies against the static test secret above.

While you're here: Zendesk's retry policy will surprise you

Related: retry-schedule comparison across 21 providers, Stripe signature failures (the unix-timestamp cousin), and webhooks to localhost for developing your handler behind the bin.

No signup needed. Or from your terminal: curl https://hookden.pages.dev/new

← All guides · Signature debugger · Payload examples · Docs · Hookden vs webhook.site