# Vimeo Webhooks

## When to Use This Skill

* How do I receive Vimeo webhooks?
* How do I verify the Vimeo `X-Webhook-Signature` header?
* Why does my Vimeo webhook signature verification fail, or why are deliveries unsigned?
* How do I handle `video-transcode-complete`, `video-deleted`, or `live-event-started` events?
* How do I set a `secret` on a Vimeo webhook through the API?

## Verification (core)

Vimeo signs a delivery only if the webhook has a `secret` set. The `X-Webhook-Signature` header is the lowercase hex HMAC-SHA256 of the **raw request body**, keyed with that secret. There is no timestamp in the signed content and no prefix on the header value.

Node (check the format first: `Buffer.from(x, 'hex')` silently truncates invalid hex):

```javascript
const crypto = require('crypto');

function verifyVimeo(rawBody, signature, secret) {
  if (!signature || !/^[0-9a-f]{64}$/i.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  // Equal-length lowercase hex strings, compared in constant time
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature.toLowerCase()));
}

```

Python (encode both sides to bytes so a non-ASCII header returns 401, not 500):

```python
import hashlib, hmac

def verify_vimeo(raw_body: bytes, signature: str | None, secret: str) -> bool:
    if not signature:
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected.encode(), signature.lower().encode())

```

> **For complete handlers with route wiring, event dispatch, and tests**, see:
> 
> * [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vimeo-webhooks/examples/express/) (read the body with `express.raw({ type: '*/*' })` so any content type verifies)
> * [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vimeo-webhooks/examples/nextjs/)
> * [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/vimeo-webhooks/examples/fastapi/)

## Setup: set a secret on the webhook

Vimeo has no dashboard for app webhooks; you create them through the API (`POST /apps/{app_id}/webhooks`) and set `secret` in the same request, or later with `PATCH /apps/{app_id}/webhooks/{webhook_id}`. Vimeo recommends a random string of at least 64 bytes. **Set a secret on the webhook through the Vimeo API. Without one, deliveries are unsigned and fail verification** (whether in your handler or in a Hookdeck source with verification on). See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/setup.md) for the requests.

For Hookdeck, create a source of type Vimeo and paste the same `secret` as its webhook signing secret.

## Common Event Types

The event name is in the body's `webhook_type` field (not a header). Payloads look like `{ "webhook_type": "video-transcode-complete", "data": { ... }, "timestamp": 1644961452 }`.

| Event (`webhook_type`) | Fires when |
| --- | --- |
| `video-created` | A user creates a video |
| `video-deleted` | A user deletes a video |
| `video-transcode-playable` | A video is ready for playback |
| `video-transcode-complete` | A video finished transcoding |
| `video-upload-failed` | A video upload fails |
| `transcript-status-complete` | A video's transcript completed or failed |
| `live-event-started` | A live event goes live |
| `live-event-ended` | A live event ends |

Other documented types: `live-event-updated`, `live-event-archive-available`, `live-event-clip-created`, `video-transcode-fully-playable`, `automatic-thumbnail-available`, `content-scan-completed`, `registration-created`, `registration-updated`, `registration-deleted`.

> **For the full event reference**, see [Vimeo app webhooks](https://developer.vimeo.com/api/app-webhooks) and [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/overview.md).

## Important Headers

| Header | Description |
| --- | --- |
| `X-Webhook-Signature` | Hex HMAC-SHA256 of the raw body; only sent when the webhook has a `secret` |
| `Content-Type` | Content type of the payload body |

Vimeo documents no delivery ID header. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/overview.md) for deduplicating.

## Environment Variables

```bash
VIMEO_WEBHOOK_SECRET=your_webhook_secret   # The `secret` you set on the webhook via the Vimeo API

```

## Local Development

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

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/overview.md) - Vimeo webhook concepts, events, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/setup.md) - Creating the webhook and its secret through the API
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/vimeo-webhooks/references/verification.md) - Signature verification details and gotchas