If you’ve ever dropped a Vimeo embed onto a page and then wired up the Vimeo Player API, you’ve probably hit the classic wall: the iframe renders, but the API fails in weird ways, or the iframe is blocked entirely by CSP.
This happens a lot because Vimeo embeds are one of those features that cross several CSP directives at once. You’re not just allowing a script. You’re allowing a framed document, cross-origin messaging, and sometimes extra assets depending on how you load the player.
I’ve seen teams burn time tweaking script-src when the actual problem was frame-src, or cargo-cult in https: everywhere just to “make it work.” That usually fixes the symptom and wrecks the policy.
Here are the most common mistakes with CSP for Vimeo Player API, and the fixes that actually hold up.
The baseline: what Vimeo Player API usually needs
A typical Vimeo setup looks like this:
<iframe
src="https://player.vimeo.com/video/76979871"
width="640"
height="360"
frameborder="0"
allow="autoplay; fullscreen; picture-in-picture"
allowfullscreen>
</iframe>
<script type="module">
import Player from "https://player.vimeo.com/api/player.js";
const iframe = document.querySelector("iframe");
const player = new Player(iframe);
player.on("play", () => {
console.log("played");
});
</script>
From a CSP perspective, the main things are:
frame-srcforhttps://player.vimeo.comscript-srcif you load Vimeo’s API script from their origin- sometimes
connect-srcdepending on your surrounding app and reporting setup - maybe
img-srcormedia-srcin stricter deployments, though the player iframe usually owns its own subresources
A minimal starting point often looks like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://player.vimeo.com;
frame-src https://player.vimeo.com;
object-src 'none';
base-uri 'self';
That’s the clean version. Real policies are rarely that clean.
Mistake #1: Allowing the script, but not the iframe
This is the big one.
I keep seeing policies where developers add Vimeo to script-src and assume they’re done. But the embed itself is an iframe, so the browser checks frame-src, not script-src.
Here’s a real-world style policy shape, based on the header from headertest.com:
content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-MjFkMjVjZjUtODllMy00MzEwLTljYWEtYjlmNjk2YzA4NjVj' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
If you add a Vimeo embed to this site, it will fail. Why? Because frame-src only allows:
frame-src 'self' https://consentcdn.cookiebot.com;
No Vimeo origin is allowed.
Fix
Add Vimeo’s player origin to frame-src:
frame-src 'self' https://consentcdn.cookiebot.com https://player.vimeo.com;
If you want to keep it minimal and explicit, that’s the right fix.
If you need a refresher on how frame-src behaves, https://csp-guide.com/frame-src/ is a good reference.
Mistake #2: Forgetting that default-src becomes the fallback
When frame-src is missing, the browser falls back to default-src.
That sounds convenient until you realize your default-src is often tighter or just unrelated to Vimeo.
For example:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://player.vimeo.com;
This will still block the Vimeo iframe because frame-src is absent, so the browser falls back to:
default-src 'self';
And https://player.vimeo.com is not 'self'.
Fix
Declare frame-src explicitly:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://player.vimeo.com;
frame-src https://player.vimeo.com;
My rule: if your page embeds anything third-party, always write frame-src on purpose. Don’t rely on fallback behavior unless you enjoy debugging CSP in production.
Mistake #3: Using the Vimeo iframe, but loading the API script from somewhere else
A lot of people copy snippets from old codebases, bundlers, or wrapper libraries and forget where the actual player API script comes from.
If you use Vimeo’s hosted player API directly, you may have code like this:
<script src="https://player.vimeo.com/api/player.js"></script>
If your script-src doesn’t allow https://player.vimeo.com, the script gets blocked even though the iframe itself is allowed.
Fix
Allow the script origin too:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://player.vimeo.com;
frame-src https://player.vimeo.com;
If you bundle the Vimeo player library into your own assets, you may not need the Vimeo host in script-src at all. But the iframe still needs frame-src.
That distinction matters. Don’t blindly add third-party script sources if your build pipeline already serves the code locally.
Official docs for the player API are here: https://developer.vimeo.com/player/sdk/reference
Mistake #4: Breaking your own nonce setup with ad hoc inline code
A lot of modern sites use nonces and strict-dynamic, similar to the headertest.com header:
script-src 'self' 'nonce-MjFkMjVjZjUtODllMy00MzEwLTljYWEtYjlmNjk2YzA4NjVj' 'strict-dynamic' ...
Then someone adds a quick inline Vimeo init block:
<script>
const player = new Vimeo.Player('demo');
</script>
That gets blocked because the inline script has no nonce.
Fix
Nonce the inline script:
<script nonce="{{ .CSPNonce }}">
const iframe = document.querySelector('#vimeo-frame');
const player = new Vimeo.Player(iframe);
</script>
Or better, move the initialization into your own external script served from 'self'.
If you’re using nonces correctly, random inline snippets are where CSP starts to rot. I try to avoid them unless there’s a very good reason.
For a deeper breakdown of script-src, https://csp-guide.com/script-src/ is worth keeping handy.
Mistake #5: Adding https://*.vimeo.com everywhere
This is the “make the error go away” fix. It works, but it’s sloppy.
I’ve seen policies like this:
script-src 'self' https://*.vimeo.com;
frame-src https://*.vimeo.com;
connect-src 'self' https://*.vimeo.com;
img-src 'self' https://*.vimeo.com;
That’s usually broader than necessary.
Fix
Start with the exact origin you need:
frame-src https://player.vimeo.com;
script-src 'self' https://player.vimeo.com;
Only expand if you have a confirmed violation showing another required origin.
CSP works best when you respond to actual browser violations, not guesses. Wildcards are sometimes necessary, but they should be the last move, not the first.
Mistake #6: Confusing frame-ancestors with frame-src
These two get mixed up constantly.
frame-srccontrols what your page is allowed to embedframe-ancestorscontrols who is allowed to embed your page
If Vimeo won’t load inside your page, changing frame-ancestors does nothing.
Using the headertest.com example again:
frame-ancestors 'none';
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none' means nobody can iframe your page. It does not grant permission to iframe Vimeo.
Fix
Leave frame-ancestors alone unless your page embedding model needs to change. Add Vimeo to frame-src instead:
frame-src 'self' https://consentcdn.cookiebot.com https://player.vimeo.com;
That’s it.
Mistake #7: Ignoring the actual CSP violation message
CSP debugging gets much easier when you stop guessing and read the console error literally.
Typical failures look like:
Refused to frame 'https://player.vimeo.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".
Or:
Refused to load the script 'https://player.vimeo.com/api/player.js' because it violates the following Content Security Policy directive: "script-src 'self' ...".
Those messages tell you exactly which directive failed. Don’t patch three directives when one is the problem.
If you want cleaner visibility, set up reporting while testing:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://player.vimeo.com;
frame-src https://player.vimeo.com;
report-to csp-endpoint;
Official CSP docs are here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
A practical policy for Vimeo Player API
If you’re embedding Vimeo and loading their player API script directly, this is a solid starting point:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://player.vimeo.com;
frame-src https://player.vimeo.com;
img-src 'self' data: https:;
style-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
If your site already has a larger policy, just merge the Vimeo parts carefully:
frame-src ... https://player.vimeo.com;
script-src ... https://player.vimeo.com;
And if you’re using a nonce-based setup, make sure your Vimeo initialization code is either nonced or external.
The short version
When Vimeo Player API breaks under CSP, the problem is usually one of these:
- missing
frame-src https://player.vimeo.com - missing
script-src https://player.vimeo.com - relying on
default-srcfallback - adding inline init code without a nonce
- widening sources too much with
*.vimeo.com - editing
frame-ancestorsinstead offrame-src
If I had to give one opinionated rule: treat third-party embeds as frame-src work first, script-src work second. That mental model saves a lot of time.