Building a secure course experience requires a thoughtful approach to both content access and video protection. By combining smart page-level gating with Vimeo’s robust domain restrictions, you can keep your premium content accessible and protected while delivering a seamless, professional experience for every paying student.
A gated course has two locks, and most builds only support one. The page checks whether somebody paid, hides the lesson if they didn't, and everyone involved feels the content is protected.
Then a student opens the browser network tab, copies the player URL, and sends it to a friend. The page gate did its job perfectly, and the video plays anyway, because the page was never what was holding it.
This guide builds the second lock: the Vimeo privacy settings that decide where a video can play at all.
What do you need to gate Vimeo videos on Webflow?
You need a paid Vimeo plan, a Webflow Cloud project that already knows who is signed in, and the video IDs. This assumes an entitlement layer rather than building one, because it is a separate job from video privacy.
Here’s the full list before you start:
- A paid Vimeo plan, since Embed only and Unlisted are both paid features, while Public and Private are on every plan
- A Webflow Cloud project running Next.js 15 or higher, with Node.js 22 or later locally
- A working way to tell whether the current visitor has bought the course, whether that is a membership table, a subscription state, or a role claim
- The custom domain your course pages will actually be served from, since that is what you will allow-list
If you haven't built the membership layer yet, our Supabase membership guide covers tier-based access, and this guide picks up where you can already answer whether a given student is entitled to a given lesson. Here's how the video half fits on top.
5 steps to build gated course video with Vimeo and Webflow
The build includes a privacy setting, a domain allow-list, a page that checks entitlement, an embed, and a habit of testing what you are actually worried about.
The first two steps happen entirely inside Vimeo, and they do the protecting.
1. Set each course video to Embed only
Open the video's settings in Vimeo and select Share. In the modal that opens, use the Privacy dropdown (not the Embed tab) and choose Embed only. This setting makes a video embeddable on other sites while returning nothing on vimeo.com, which Vimeo describes as helpful for businesses that host videos on their own domains.
The Embed tab is where you restrict domains, which is the next step.
Two naming notes save time here. Older tutorials refer to "Hide this from Vimeo.com", which is the older label for this behaviour and still turns up in Vimeo's own help content, so follow the description rather than the label you remember.
If you set the video to Unlisted at any point, Vimeo added a privacy hash to its URL that must be included for embeds and sharing to work, so an embed that worked yesterday can break when you change privacy.
You finish this step with a video that returns nothing on vimeo.com but still embeds.
2. Restrict the video to your domains
Still in the Embed panel, open the dropdown under "Where can this be embedded?" and choose Specific domains, then add each domain the course is served from in the Allowed domains box and save with the plus button or the Enter key.
This step turns a copied embed code into a dead link on somebody else's site. Vimeo lets you specify up to 50 domains, and any site not on the list gets an error when it tries to embed the video.
Add every domain you genuinely serve from, including the staging subdomain if you preview there, because a missing entry looks exactly like a broken video. Do not add domains you do not control. You finish this step with a video that plays on your course pages and errors everywhere else.
3. Gate the lesson page itself
With the video locked to your domain, the page gate goes back to its proper job: deciding what a signed-in student can see and keeping unpaid visitors away from the lesson content around the video.
The page checks entitlement before rendering anything:
// app/lessons/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { getLesson } from '@/lib/lessons'
import { hasAccessToLesson } from '@/lib/entitlement'
import UpgradePrompt from '@/components/UpgradePrompt'
type Props = { params: Promise<{ slug: string }> }
export default async function LessonPage({ params }: Props) {
const { slug } = await params
const lesson = await getLesson(slug)
if (!lesson) notFound()
// Gate the page. This stops the lesson rendering, but it is not
// what stops the video playing elsewhere: that is Vimeo's job.
if (!(await hasAccessToLesson(lesson.id))) {
return <UpgradePrompt lesson={lesson} />
}
return (
<article>
<h1>{lesson.title}</h1>
<div style={{ position: 'relative', paddingTop: '56.25%' }}>
<iframe
// Store the full player URL Vimeo gives you, hash and all.
// Rebuilding it from a bare ID breaks any video that ever
// passed through Unlisted.
src={lesson.vimeoPlayerUrl}
allow="autoplay; fullscreen; picture-in-picture"
title={lesson.title}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
border: 0,
}}
/>
</div>
</article>
)
}
Notice what the comment in that handler is admitting. The page gate is not the security boundary for the video, and pretending otherwise is how the leak happened in the first place. It is the boundary for everything else on the page: the transcript, the downloads, the next-lesson link, the discussion.
Rendering an upgrade prompt rather than a 404 is usually the better product decision, since a student who paid for tier one and clicked a tier-two lesson is a sales conversation, not an error. You finish this step with a lesson page that shows the video to entitled students and a prompt to everyone else.
4. Keep the video ID out of the public bundle
The embed needs a Vimeo video ID, and the instinct is to put every lesson's ID in the CMS or a JSON file the client can read. That undoes some of the work you just did.
A video ID isn't a secret like an API key, because domain-level privacy already stops it from playing elsewhere. But a complete list of every lesson ID handed to unauthenticated visitors tells anybody exactly how large the course is and gives them a map to test against. Fetch the ID server-side for the lesson being viewed, as the page above does, rather than shipping the catalog.
This matters more once you sell tiers. If the free tier's page bundle contains the IDs for premium lessons, you have published the structure of what you are selling. You finish this step with a page whose source shows one video ID: the one the student is entitled to watch.
5. Test the leak you are worried about
Most course builds are tested by signing in and confirming the video plays. That verifies the happy path, not the protection.
Test the real scenario instead. Copy the player URL from the network tab, open it in a private window, and confirm you get Vimeo's error instead of your lesson. Then paste the embed code into a scratch HTML file served from a different domain, or a CodePen, and confirm it fails there too. If either plays, the domain list is wrong, or the privacy setting didn't save.
Do this again after any change to your domains, because moving from a staging domain to a custom one can silently break either the protection or the playback, depending on which list you forgot to update. You finish this step having watched the video fail where it should.
What causes gated Vimeo videos to fail on Webflow?
Failures split cleanly into two: the video plays where it shouldn't, or it refuses to play where it should. Both come from the same two settings, so it's worth checking them together.
These four cover nearly all of it, and the first costs revenue rather than support time.
The video plays for people who did not pay
Cause: The video is Unlisted rather than Embed only. Unlisted hides a video from search while leaving it shareable by URL and embeddable anywhere. So the page gate is the only thing standing in the way, and it's trivially bypassed by copying the player link.
Fix: Change the privacy to Embed only and add domain-level privacy, then re-test by opening the player URL directly in a private window. Be aware that anyone who already has the link keeps working access until you change the setting, so treat a known leak as a reason to act the same day rather than at the next content update.
Students see an error instead of the lesson
Cause: The domain serving the page is not on the allowed domains list. Vimeo returns an error to any site not listed, including subtle cases like a staging subdomain, a preview URL, or the apex domain when you only listed the www version.
Fix: Add every domain you serve from, up to the 50-domain limit. Don't trust the address bar for the hostname: open the Network tab, find the player request returning 403, and read the Referrer value from its request headers, since that is the domain Vimeo actually saw.
If the video plays on one environment and fails on another, the usual culprit on Webflow Cloud is a preview environment hostname that never made it onto the list.
Cause: Your own site is suppressing the referrer, so a correctly listed domain still fails. Vimeo documents this as the first thing to check, and it catches Next.js apps easily: a <meta name="referrer" content="no-referrer"> tag or a Referrer-Policy: same-origin header stops the cross-origin player request from carrying a referrer at all, which leaves Vimeo unable to verify your domain.
Fix: Read that same Referrer-Policy header. If it is missing or blank rather than wrong, the problem is your policy and not your allow-list. Replace a no-referrer meta tag with <meta name="referrer" content="origin"> and loosen the header. Note, too, that some third-party embedding contexts strip the referrer outright, and domain privacy cannot work there at all.
The embed worked yesterday and is blank today
Cause: The privacy hash changed or was dropped. This is a migration problem rather than a steady-state one: Vimeo adds the hash when a video is Unlisted, and it must be present for embeds and sharing to work, so a library that passed through Unlisted on its way to Embed-only can carry stored URLs whose hash no longer matches.
Fix: Regenerate the embed code from Vimeo after any privacy change rather than reusing a stored URL, and store whatever Vimeo currently gives you rather than reconstructing player URLs by hand from an ID. If you build URLs from IDs, a hash-bearing video will break, and nothing in your code will look wrong.
Nobody can watch, including you
Cause: The video is set to Private. That setting restricts viewing to you and your team members on the account, and Vimeo states other people cannot see it even when it is embedded on another website, so it is too restrictive for a course with customers.
Fix: Use Embed only with domain-level privacy instead. Private is the right setting for internal review copies and the wrong one for anything you sell, and the two get confused because both sound like the secure option.
The tell is that Private fails for everyone equally, so if your own test account cannot watch an embedded lesson either, the problem is the privacy setting rather than your entitlement logic. Check that before you dig into the gate code, because an access bug and an over-restrictive privacy setting can look identical on the page.
What you can build next with Vimeo and Webflow
With the video locked to your domains, the additions are about the experience around it: progress tracking per lesson, a resume-where-you-left-off position, chapters, or downloadable transcripts released on the same entitlement check the page already runs.
If your course sells as a subscription rather than a one-off, our Stripe subscription guide covers deriving access from subscription state, which is what this page calls. For the no-code connection routes, see the Webflow and Vimeo integration.
Frequently asked questions
Is Unlisted good enough for a paid course?
No. Vimeo documents Unlisted as accessible and shareable by anyone with the URL, and embeddable anywhere online. It hides a video from search, which is not the same as protecting it. A forwarded link works for whoever receives it.
Does domain-level privacy need a specific Vimeo plan?
No. Vimeo states the feature is included with all plans, and you can list up to 50 domains. The privacy setting itself is the plan-dependent part, since Embed only is a paid feature while Public and Private are on every plan.
Why not just hide the video behind my page gate?
Because the page and the video are served separately. Your gate controls the page; Vimeo controls the player. Anyone who copies the player URL bypasses the first entirely, which is why the protection has to live at Vimeo.
Should I use Private for course videos?
No. Private limits viewing to you and your team, and the video stays unwatchable when embedded elsewhere, except for team members you have granted permission to, so your students cannot see it either. Embedding only with domain restrictions is the setting that fits a course.




