# 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_validation` handshake?
* How do I handle `meeting.started`, `meeting.ended`, or `recording.completed` events?
* 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.

```javascript
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:

```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:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoom-webhooks/examples/express/)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoom-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/zoom-webhooks/examples/fastapi/)

## 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](https://developers.zoom.us/docs/api/webhooks/).

## 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

```bash
ZOOM_WEBHOOK_SECRET_TOKEN=your_secret_token   # From your app's Feature/Webhook page in the Zoom App Marketplace

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 zoom --path /webhooks/zoom

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoom-webhooks/references/overview.md) - Zoom webhook concepts and common events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoom-webhooks/references/setup.md) - App Marketplace configuration guide
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/zoom-webhooks/references/verification.md) - Signature verification and url_validation details