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.
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.
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:
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.
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.
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.
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.
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.
429/503 retry only when accompanied by a
retry-after header under 60 seconds; timeouts (12-second limit) retry up to
5×. Any other 4xx/5xx is dropped — no retry, no dead-letter.X-Zendesk-Webhook-Invocation-Id or the event id.GET /api/v2/webhooks/{id}/invocations — check it before assuming events
never fired.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