Agent skill
Zoom Webhooks Skill
Receive and verify Zoom webhooks. Use when setting up Zoom webhook handlers, debugging signature verification, completing the endpoint.url_validation handshake, or handling meeting and recording events like meeting.started, meeting.ended, meeting.participant_joined, or recording.completed.
Install this skill
npx skills add hookdeck/webhook-skills --skill zoom-webhooks
When to Use This Skill
- How do I receive Zoom webhooks?
- How do I verify Zoom webhook signatures?
- How do I complete the Zoom
endpoint.url_validationhandshake? - How do I handle
meeting.started,meeting.ended, orrecording.completedevents? - Why is my Zoom webhook signature verification failing?
Verification (core)
Zoom signs each webhook with HMAC-SHA256 (hex) keyed on your app's Secret Token. Build the message v0:{x-zm-request-timestamp}:{raw body}, hash it, and prefix with v0=. Compare against the x-zm-signature header timing-safe. Always use the raw body — parsing to JSON first changes the bytes and breaks the signature.
Zoom also requires a one-time URL validation handshake: when event === "endpoint.url_validation", respond 200 with { plainToken, encryptedToken } where encryptedToken = HMAC-SHA256(plainToken, secretToken) in hex. Respond to every webhook within 3 seconds.
const crypto = require('crypto');
// Verify the x-zm-signature header against v0:{timestamp}:{rawBody}
function verifyZoomWebhook(rawBody, timestamp, signature, secretToken) {
if (!timestamp || !signature) return false;
const message = `v0:${timestamp}:${rawBody}`;
const expected = 'v0=' + crypto.createHmac('sha256', secretToken).update(message).digest('hex');
try {
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
} catch {
return false;
}
}
// Answer the one-time endpoint.url_validation challenge
function validationResponse(plainToken, secretToken) {
const encryptedToken = crypto.createHmac('sha256', secretToken).update(plainToken).digest('hex');
return { plainToken, encryptedToken };
}
Python:
import hmac, hashlib
def verify_zoom_webhook(raw_body: bytes, timestamp: str, signature: str, secret_token: str) -> bool:
if not timestamp or not signature:
return False
message = b"v0:" + timestamp.encode() + b":" + raw_body
expected = "v0=" + hmac.new(secret_token.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(signature, expected)
def validation_response(plain_token: str, secret_token: str) -> dict:
encrypted = hmac.new(secret_token.encode(), plain_token.encode(), hashlib.sha256).hexdigest()
return {"plainToken": plain_token, "encryptedToken": encrypted}
For complete handlers with route wiring, the url_validation handshake, event dispatch, and tests, see:
Common Event Types
| Event | Triggered When |
|---|---|
endpoint.url_validation | Zoom validates your endpoint (one-time handshake, must be answered) |
meeting.started | A meeting starts |
meeting.ended | A meeting ends |
meeting.participant_joined | A participant joins a meeting |
meeting.participant_left | A participant leaves a meeting |
recording.completed | A cloud recording finishes processing |
For the full event reference, see Zoom Webhook Events.
Important Headers
| Header | Description |
|---|---|
x-zm-signature | Signature v0=<hex> over v0:{timestamp}:{rawBody} |
x-zm-request-timestamp | Unix timestamp included in the signed message |
Environment Variables
ZOOM_WEBHOOK_SECRET_TOKEN=your_secret_token # From your app's Feature/Webhook page in the Zoom App Marketplace
Local Development
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 zoom --path /webhooks/zoom
Reference Materials
- references/overview.md - Zoom webhook concepts and common events
- references/setup.md - App Marketplace configuration guide
- references/verification.md - Signature verification and url_validation details