Zoom webhook validation failed? Here's the CRC, decoded

You paste your Event Notification Endpoint URL into your Zoom app, click Validate, and get failed to validate. Zoom's URL validation is a challenge-response check (CRC) — and unlike Slack's, you can't pass it by just echoing a value back. You have to hash it. Here's exactly what Zoom expects, the handful of ways it goes wrong, and how to pass it with zero code while you inspect the real traffic.

What Zoom actually sends

When you click Validate (and again automatically every 72 hours — more on that below), Zoom POSTs this to your endpoint:

{
  "event": "endpoint.url_validation",
  "payload": { "plainToken": "qgg8vlvZRS6UYooatFL8Aw" },
  "event_ts": 1658940994914
}

Your endpoint must respond within 3 seconds, with status 200 (or 204), and a JSON body containing the plain token and its HMAC-SHA256, keyed by your app's webhook secret token, hex-encoded:

{
  "plainToken": "qgg8vlvZRS6UYooatFL8Aw",
  "encryptedToken": "23a89b634c017e5364a1c8d9c8ea909b60dd5599e2bb04bb1558d9c3a121faa5"
}

The five classic failures

Pass it with zero code (and see the traffic)

Chicken-and-egg: you want to see Zoom's events before you build the endpoint, but Zoom won't send events until an endpoint passes validation. Most capture tools can't help — an echo template can return plainToken, but not its HMAC. Hookden's response templates can compute the hash:

  1. Create a bin (no signup needed) and open its settings.
  2. Set the custom response — Status 200, Content-Type application/json, Body:
{"plainToken":"{{body.payload.plainToken}}","encryptedToken":"{{hmac_sha256 body.payload.plainToken YOUR_SECRET_TOKEN}}"}
  1. Replace YOUR_SECRET_TOKEN with the webhook secret token from your app's Feature page. Leave the response delay at 0 (the 3-second deadline is real).
  2. Paste your https://hookden.pages.dev/h/… URL into the Event Notification Endpoint URL field and click Validate.

The {{hmac_sha256 var secret}} template helper computes a hex HMAC-SHA256 of any request field at response time (template docs) — so the CRC passes, and every Zoom event after it lands in your dashboard as inspectable JSON.

The secret sits in your bin's response settings, so treat it as a development secret and rotate it in Zoom when you move to production.

The 72-hour revalidation trap

Passing once isn't enough: Zoom automatically revalidates your endpoint every 72 hours, and emails the account owner when revalidation fails. Two practical consequences:

Verifying real events: x-zm-signature

Once validated, every Zoom webhook carries x-zm-request-timestamp and x-zm-signature: v0=<hex>. The signature is HMAC-SHA256 (same secret token) over the string v0:{timestamp}:{raw body}:

const msg = `v0:${req.headers["x-zm-request-timestamp"]}:${rawBody}`;
const sig = "v0=" + crypto.createHmac("sha256", SECRET_TOKEN).update(msg).digest("hex");
// compare to req.headers["x-zm-signature"]

Classic bug: hashing JSON.stringify(req.body) after your framework re-parsed the body. If key order or whitespace changed, the signature won't match — HMAC the raw bytes. Capture a real delivery in your bin first and you can see the exact body and headers Zoom sends, byte for byte.

Zoom's retry math (why you missed an event)

Related handshakes: Slack URL verification (echo-style — no HMAC needed) and Discord's Ed25519 check (signature-style — stricter still).

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

← All guides · Docs · Hookden vs webhook.site