Guide to Vimeo Webhooks: Features and Best Practices
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
| Feature | Details |
|---|---|
| Configuration | Vimeo API only: POST /apps/{app_ |
| Subscription | One webhook per event type per app |
| Payload | { webhook_, with resource URIs in data |
| Signature header | X-Webhook-Signature, only when the webhook has a secret |
| Scheme | HMAC-SHA256, hex, no prefix |
| Signed content | The raw request body, nothing else |
| Secret | The secret you set on the webhook through the API |
| Rate limit | 25 to 2,500 requests per webhook per minute, by the app owner's plan |
| Rate limit headers | X-Webhook-RateLimit-Limit, X-Webhook-RateLimit-Remaining, X-Webhook-RateLimit-Reset |
| Replay window | None. 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.
| Area | Events | Fires when |
|---|---|---|
| Videos | video-created, video-deleted | A user creates or deletes a video |
| Transcoding | video-transcode-playable, video-transcode-fully-playable, video-transcode-complete | A video is ready to play, is playable in all its resolutions, or has finished transcoding |
| Upload problems | video-upload-failed | An upload fails |
| Processing | transcript-status-complete, automatic-thumbnail-available, content-scan-completed | A transcript completes or fails, a generated thumbnail is ready, or a content scan of a version finishes |
| Live events | live-event-started, live-event-updated, live-event-ended, live-event-archive-available, live-event-clip-created | A live event goes live, is edited or ends, its archive is ready, or a clip is created from it |
| Registrations | registration-created, registration-updated, registration-deleted | A 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_typeis the event. Route on it.dataholds URIs, not full resources. Call the API (for exampleGET /videos/890352789) for titles, files or status.timestampis the Unix time in seconds when the event happened.- The fields inside
datavary by event.video-upload-failedcarriesvideo_uri,error_type(for exampleinvalid-video-file) andfailure_cause(for exampleremote_unauthorized).transcript-status-completecarriestranscript_uri,text_track_uri,transcript_statusandvideo_uri. Registration events carryresource_uri,registrant_uuid,emailandsequence_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_SCOPEcapability, which lets its access tokens use thewebhooksscope. - An access token for that app with the
public,private,edit,deleteandwebhooksscopes.
Then:
Generate a random secret. Vimeo recommends at least 64 bytes, for example
openssl rand -hex 64.Create a webhook for each event type you need, setting
secretin the same request. Send the version 3.4Acceptheader 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>" }'Store the secret server-side, for example as
VIMEO_WEBHOOK_SECRET. All of your app's webhooks can share it.To add or change a secret on an existing webhook,
PATCH /apps/{app_with a newid}/webhooks/{webhook_ id} secret. To read it back, callGET /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_, which is where the hex encoding comes from. In a real PHP app the header arrives as $_, 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-Signatureheader 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
secretwhen creating a webhook, andPATCHone onto any older webhooks. - Reject requests without
X-Webhook-Signaturerather 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
2xxas 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-Remainingon 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 indataandtimestamp. - 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_, 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.