Gareth Wilson Gareth Wilson

Guide to Vimeo Webhooks: Features and Best Practices

Published


Vimeo app webhooks tell your application when something happens to the videos and live events your users manage through it: an upload finishes transcoding, a video is deleted, a transcript is ready, a live stream starts or ends, or someone registers for a gated event. Instead of polling the Vimeo API for status changes, your app gets a short notification with the URIs to fetch.

This guide covers how Vimeo webhooks are created and signed, the events and their payloads, the rate limits that apply to deliveries, and the practices that keep a Vimeo receiver reliable.

What are Vimeo webhooks?

Vimeo webhooks belong to a Vimeo developer app. There is no dashboard screen for them: you create, list, update and delete them through the Vimeo API. Each webhook has a webhook_url, a single webhook_type (the event), an is_enabled flag and an optional secret. An app can have only one webhook of a given event type, so subscribing to five events means five webhooks.

A webhook fires when a user who has authorised your app, with the webhooks scope, performs the action: uploading a video, deleting one, starting a live event and so on. Vimeo then POSTs a small JSON body naming the event and the URIs of the affected resources.

If the webhook has a secret, Vimeo adds an X-Webhook-Signature header: an HMAC-SHA256 of the raw payload, keyed with that secret. Without a secret, Vimeo sends no signature at all.

Vimeo webhook features

FeatureDetails
ConfigurationVimeo API only: POST /apps/{app_id}/webhooks
SubscriptionOne webhook per event type per app
Payload{ webhook_type, data, timestamp }, with resource URIs in data
Signature headerX-Webhook-Signature, only when the webhook has a secret
SchemeHMAC-SHA256, hex, no prefix
Signed contentThe raw request body, nothing else
SecretThe secret you set on the webhook through the API
Rate limit25 to 2,500 requests per webhook per minute, by the app owner's plan
Rate limit headersX-Webhook-RateLimit-Limit, X-Webhook-RateLimit-Remaining, X-Webhook-RateLimit-Reset
Replay windowNone. The body's timestamp is not checked by Vimeo

Vimeo doesn't document a retry policy, a response timeout, the status codes it treats as success, a delivery ID or source IPs for webhooks.

Common events

Vimeo documents 17 event types. The name you subscribe with is also the webhook_type value in each delivery.

AreaEventsFires when
Videosvideo-created, video-deletedA user creates or deletes a video
Transcodingvideo-transcode-playable, video-transcode-fully-playable, video-transcode-completeA video is ready to play, is playable in all its resolutions, or has finished transcoding
Upload problemsvideo-upload-failedAn upload fails
Processingtranscript-status-complete, automatic-thumbnail-available, content-scan-completedA transcript completes or fails, a generated thumbnail is ready, or a content scan of a version finishes
Live eventslive-event-started, live-event-updated, live-event-ended, live-event-archive-available, live-event-clip-createdA live event goes live, is edited or ends, its archive is ready, or a clip is created from it
Registrationsregistration-created, registration-updated, registration-deletedA registrant is added to, changed on or removed from a registration-gated resource

Here is Vimeo's video-transcode-complete example:

{
  "webhook_type": "video-transcode-complete",
  "data": {
    "clip_uri": "/videos/890352789",
    "video_uri": "/videos/890352789",
    "version_uri": "/videos/890352789/versions/1"
  },
  "timestamp": 1644961452
}
  • webhook_type is the event. Route on it.
  • data holds URIs, not full resources. Call the API (for example GET /videos/890352789) for titles, files or status.
  • timestamp is the Unix time in seconds when the event happened.
  • The fields inside data vary by event. video-upload-failed carries video_uri, error_type (for example invalid-video-file) and failure_cause (for example remote_unauthorized). transcript-status-complete carries transcript_uri, text_track_uri, transcript_status and video_uri. Registration events carry resource_uri, registrant_uuid, email and sequence_number.

Registration payloads include the registrant's email address, so treat them as personal data in logs and queues.

Setting up Vimeo webhooks

Before you start, you need:

  • A Vimeo app at developer.vimeo.com/apps with the CAPABILITY_WEBHOOK_SCOPE capability, which lets its access tokens use the webhooks scope.
  • An access token for that app with the public, private, edit, delete and webhooks scopes.

Then:

  1. Generate a random secret. Vimeo recommends at least 64 bytes, for example openssl rand -hex 64.

  2. Create a webhook for each event type you need, setting secret in the same request. Send the version 3.4 Accept header with every call:

    curl -X POST "https://api.vimeo.com/apps/$APP_ID/webhooks" \
      -H "Authorization: bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Accept: application/vnd.vimeo.*+json;version=3.4" \
      -d '{
        "is_enabled": true,
        "webhook_url": "https://example.com/webhooks/vimeo",
        "webhook_type": "video-transcode-complete",
        "secret": "<your secret>"
      }'
    
  3. Store the secret server-side, for example as VIMEO_WEBHOOK_SECRET. All of your app's webhooks can share it.

  4. To add or change a secret on an existing webhook, PATCH /apps/{app_id}/webhooks/{webhook_id} with a new secret. To read it back, call GET /apps/{app_id}/webhooks/{webhook_id}?fields=secret.

Set a secret on every webhook through the Vimeo API. The secret field is optional, and a webhook created without one sends no X-Webhook-Signature header, so its deliveries fail verification in your handler and in a Hookdeck Vimeo source.

If you receive Vimeo webhooks through Hookdeck, use the Hookdeck source URL as each webhook's webhook_url and paste the same secret into the Vimeo source.

Vimeo has no test-delivery button. Trigger a real event, such as uploading and deleting a short test video, to see your first delivery.

Securing Vimeo webhooks

Compute an HMAC-SHA256 of the raw request body with the secret as the key, hex-encode it, and compare it with X-Webhook-Signature in constant time.

const crypto = require('crypto');
const express = require('express');

function verifyVimeoSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/vimeo', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.VIMEO_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyVimeoSignature(req.body, req.get('x-webhook-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // Route on event.webhook_type, then process asynchronously.
  res.sendStatus(200);
});

The same check in Python:

import hashlib
import hmac


def verify_vimeo_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature_header.strip().lower().encode("utf-8"), expected.encode("utf-8"))


# In FastAPI: raw_body = await request.body()
# verify_vimeo_signature(raw_body, request.headers.get("x-webhook-signature"),
#                        os.environ["VIMEO_WEBHOOK_SECRET"])

Vimeo's docs give a one-line PHP check, hash_equals(hash_hmac('sha256', $payload, $secret), $_SERVER['X-Webhook-Signature']), which is where the hex encoding comes from. In a real PHP app the header arrives as $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'], so copy the logic rather than the array key.

To check your verifier, use the secret vimeo-guide-test-secret and this exact body:

{"webhook_type":"video-deleted","data":{"video_uri":"/videos/890352789"},"timestamp":1761400000}

The X-Webhook-Signature value should be:

13e0972d56e5fcb17e1fbcada8bb7a0f63500f6e865f351319079824ad1a36d8

Make Vimeo webhooks production-ready. Hookdeck Event Gateway verifies the X-Webhook-Signature header on every delivery, acknowledges Vimeo straight away, and holds each video and live event until your service has processed it.

Vimeo webhook limitations and pain points

Signing is optional

The Problem: Deliveries arrive with no signature, so anything could have sent them.

Why It Happens: secret is an optional field on the create request. Leave it out and Vimeo signs nothing.

Workarounds:

  • Always set secret when creating a webhook, and PATCH one onto any older webhooks.
  • Reject requests without X-Webhook-Signature rather than treating a missing header as a pass.

How Hookdeck Can Help: A Hookdeck Vimeo source with the secret configured marks unsigned or mis-signed deliveries as failed verification, so they never reach your service.

No documented retries

The Problem: You can't tell whether a delivery that hit a deploy or an outage will ever come back.

Why It Happens: Vimeo's webhook docs describe rate limits and error codes for the management API, but say nothing about retrying failed deliveries, timeouts or which status codes count as success.

Workarounds:

  • Return a 2xx as soon as the signature is verified and the event is stored.
  • After any outage, reconcile through the API, for example by checking the transcode status of recently uploaded videos.

How Hookdeck Can Help: Hookdeck accepts each delivery as soon as it arrives and stores it, then delivers to your service with retries you configure, so an outage on your side doesn't depend on Vimeo's unknown retry behaviour.

Per-webhook rate limits that drop requests

The Problem: During a bulk upload, some notifications never arrive.

Why It Happens: Each webhook is limited to a number of requests per minute set by the app owner's plan: 25 on Free, 125 on Starter, 250 on Standard, 750 on Advanced and 2,500 on Enterprise. Once a webhook hits its limit, Vimeo says it no longer sends requests to that URL until the quota resets. It doesn't say the skipped events are queued.

Workarounds:

  • Watch X-Webhook-RateLimit-Remaining on incoming deliveries and alert when it gets low.
  • After a burst, use the API to fill in anything that may have been skipped.
  • Spread large imports over time, or move the app owner to a plan with a higher limit.

How Hookdeck Can Help: Hookdeck can't raise Vimeo's limit, but it logs every delivery that does arrive, including its rate limit headers, so gaps are easy to spot and investigate.

Thin payloads and no delivery ID

The Problem: Every event needs a follow-up API call, and duplicates are hard to spot.

Why It Happens: Payloads carry URIs rather than resources, and Vimeo sends no delivery ID or event ID. Only the body is signed, and Vimeo doesn't check timestamp.

Workarounds:

  • Fetch the resource from the API before acting, which also confirms its current state.
  • Build an idempotency key from webhook_type, the main URI in data and timestamp.
  • Mind your own API rate limits when every webhook triggers a fetch.

How Hookdeck Can Help: Hookdeck's deduplication can drop repeated deliveries based on the body fields you choose, and its delivery rate limits smooth out the follow-up API calls your service makes.

Best practices

Verify against the raw body

The signature covers the exact bytes. Use express.raw({ type: '*/*' }) in Express, await request.text() in a Next.js route handler and await request.body() in FastAPI, and parse only after verification.

Manage webhooks as code

Because Vimeo webhooks only exist through the API, keep a script or infrastructure code that lists what should exist, compares it with GET /apps/{app_id}/webhooks, and creates, updates or disables webhooks to match. It is easy to lose track of one-webhook-per-type subscriptions otherwise.

Treat events as hints

Use each event to decide what to fetch, not as the source of truth. Vimeo doesn't promise delivery order, and the transcoding events for one video can fire close together, so read the video's current state from the API before updating your records.

Acknowledge fast and process asynchronously

Verify, store, return 200, and do the work afterwards. See why to process webhooks asynchronously and our guide to webhook idempotency.

Conclusion

Vimeo app webhooks are created through the API, one per event type, and are only signed when you give them a secret. Set one on every webhook, verify the hex HMAC-SHA256 of the raw body in X-Webhook-Signature, fetch the resources behind the URIs before acting, and keep an eye on the per-webhook rate limit during busy periods.

Hookdeck Event Gateway sits in front of your receiver, checks every Vimeo signature, and holds each event until your service has processed it.

Get started with Hookdeck for free and handle Vimeo webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.