ConvertKit forms are simple to embed, but they’re annoying from a CSP perspective for one reason: most teams start with a tight policy, paste in the form snippet, and then the browser blocks half of it.
I’ve had to fix this a few times. The pattern is usually the same:
- the form loader script is blocked
- inline styles or scripts get blocked
- form submission fails because
connect-srcorform-actionis too strict - success messages or embeds break inside an iframe
This guide is the practical version: what to allow, what to avoid, and copy-paste policies you can start with.
What ConvertKit usually needs
ConvertKit form embeds generally involve some combination of:
- a remote script
- inline bootstrap code
- API or form submission requests
- assets like images, fonts, or styles
- sometimes frames depending on the embed style
The exact domains can vary by embed type and by ConvertKit infrastructure changes, so you should always verify in DevTools and with CSP reports. Still, a working baseline is usually enough to get unstuck.
Start with Report-Only first
Do this before enforcing anything. It saves a lot of pain.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-src 'self'; form-action 'self'; base-uri 'self'; object-src 'none'; report-to default-endpoint; report-uri /csp-report
If you already run a broader production policy, keep your existing directives and add reporting so you can see what ConvertKit is trying to load.
A real-world CSP often already includes analytics and consent tools. For example, this header from headertest.com is pretty typical of a modern site with GTM, GA, and Cookiebot:
content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-NjU3Y2ViZDItNTg4OC00NDkyLWFmNWMtOTRmNmZjNWRjMjAz' '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'
That matters because you usually aren’t adding ConvertKit to a blank policy. You’re adding it to a policy that already has baggage.
Recommended minimum directives
For ConvertKit forms, these directives are the ones you’ll most likely touch:
script-srcconnect-srcstyle-srcimg-srcframe-srcform-action
If you use a hosted form page in an iframe or a JS embed, expect script-src and frame-src to be the first places that fail.
Copy-paste policy for a typical ConvertKit embed
This is a pragmatic starter policy for sites embedding ConvertKit forms while keeping the policy reasonably tight.
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'self';
script-src 'self' 'unsafe-inline' https://*.convertkit.com;
style-src 'self' 'unsafe-inline' https://*.convertkit.com;
img-src 'self' data: https:;
font-src 'self' data: https://*.convertkit.com;
connect-src 'self' https://*.convertkit.com;
frame-src 'self' https://*.convertkit.com;
form-action 'self' https://*.convertkit.com;
A few opinions here:
- I’m using
https://*.convertkit.combecause ConvertKit may serve assets or form endpoints from subdomains. - I included
'unsafe-inline'forscript-srcandstyle-srcbecause many embed snippets still depend on inline bootstrap code or inline styling. - If you can avoid
'unsafe-inline', do it. But don’t pretend you’re more secure while the form is broken in production.
Better version: use a nonce instead of unsafe-inline
If your app can generate a nonce per response, use that for inline script. That’s the cleaner setup.
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'self';
script-src 'self' 'nonce-r4nd0m123' https://*.convertkit.com;
style-src 'self' 'unsafe-inline' https://*.convertkit.com;
img-src 'self' data: https:;
font-src 'self' data: https://*.convertkit.com;
connect-src 'self' https://*.convertkit.com;
frame-src 'self' https://*.convertkit.com;
form-action 'self' https://*.convertkit.com;
Then apply the same nonce to the inline embed script:
<script nonce="r4nd0m123">
// ConvertKit embed bootstrap code here
</script>
<script async src="https://example.convertkit.com/some-embed.js"></script>
If you already use nonces and strict-dynamic for other third-party scripts, test carefully. strict-dynamic changes trust behavior in ways people routinely misunderstand.
Example: Nginx header
add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline' https://*.convertkit.com; style-src 'self' 'unsafe-inline' https://*.convertkit.com; img-src 'self' data: https:; font-src 'self' data: https://*.convertkit.com; connect-src 'self' https://*.convertkit.com; frame-src 'self' https://*.convertkit.com; form-action 'self' https://*.convertkit.com" always;
Example: Apache
Header always set Content-Security-Policy "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self'; script-src 'self' 'unsafe-inline' https://*.convertkit.com; style-src 'self' 'unsafe-inline' https://*.convertkit.com; img-src 'self' data: https:; font-src 'self' data: https://*.convertkit.com; connect-src 'self' https://*.convertkit.com; frame-src 'self' https://*.convertkit.com; form-action 'self' https://*.convertkit.com"
Example: Express / Node.js
app.use((req, res, next) => {
res.setHeader(
"Content-Security-Policy",
[
"default-src 'self'",
"base-uri 'self'",
"object-src 'none'",
"frame-ancestors 'self'",
"script-src 'self' 'unsafe-inline' https://*.convertkit.com",
"style-src 'self' 'unsafe-inline' https://*.convertkit.com",
"img-src 'self' data: https:",
"font-src 'self' data: https://*.convertkit.com",
"connect-src 'self' https://*.convertkit.com",
"frame-src 'self' https://*.convertkit.com",
"form-action 'self' https://*.convertkit.com"
].join("; ")
);
next();
});
If you already have a production CSP
Most teams need to merge ConvertKit into an existing policy instead of replacing it.
Here’s a realistic pattern based on the headertest-style policy shown earlier:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-r4nd0m123' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://*.convertkit.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://*.convertkit.com;
img-src 'self' data: https:;
font-src 'self' data: https://*.convertkit.com;
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 https://*.convertkit.com;
frame-src 'self' https://consentcdn.cookiebot.com https://*.convertkit.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self' https://*.convertkit.com;
object-src 'none';
That’s usually the right way to think about it: append the ConvertKit sources only to the directives that need them.
Common breakages and fixes
1. The embed script is blocked
Browser error usually looks like:
Refused to load the script 'https://...' because it violates the following Content Security Policy directive: "script-src ..."
Fix:
- add the exact ConvertKit host to
script-src - if the snippet includes inline JS, add a nonce or temporarily allow
'unsafe-inline'
2. Form submits fail silently
This one is easy to miss because the UI may still render.
Fix:
- add ConvertKit endpoints to
connect-src - if the browser performs a direct form POST, add them to
form-action
I’ve seen people add only script-src and wonder why submissions still fail. Different directive.
3. Embedded form frame is blank
Fix:
- add ConvertKit to
frame-src
If your own site is embedded elsewhere and you’ve set frame-ancestors 'none', that does not control whether you can embed ConvertKit. That controls who can embed you.
4. Styling is broken
Fix:
- add ConvertKit to
style-src - if the widget injects inline styles, allow
'unsafe-inline'for styles
I don’t love 'unsafe-inline' in style-src, but for third-party embeds it’s often the least bad option.
Tightening the policy later
Once the form works, reduce scope.
Good cleanup steps:
- replace
https:inimg-srcwith explicit hosts if you can - replace
'unsafe-inline'inscript-srcwith nonces - keep
object-src 'none' - keep
base-uri 'self' - keep
form-actionexplicit instead of relying ondefault-src
If you want ready-made policy patterns, https://csp-examples.com is useful for quick comparisons.
How to verify what ConvertKit actually uses
Use browser DevTools and watch for CSP violations on:
- initial page load
- form render
- submit
- success state
- error state
Then check:
- Network panel for blocked script, XHR/fetch, frame, and image requests
- Console for CSP violation messages
- your
report-uriorreport-toendpoint for real user failures
Third-party vendors change infrastructure. Don’t hardcode assumptions forever.
Safe baseline I’d actually ship
If I needed something practical for production today, I’d start here:
Content-Security-Policy:
default-src 'self';
base-uri 'self';
object-src 'none';
frame-ancestors 'self';
script-src 'self' 'unsafe-inline' https://*.convertkit.com;
style-src 'self' 'unsafe-inline' https://*.convertkit.com;
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self' https://*.convertkit.com;
frame-src 'self' https://*.convertkit.com;
form-action 'self' https://*.convertkit.com;
Then I’d switch script-src to nonce-based as soon as I confirmed how the embed is injected.
Official docs
For CSP directive behavior and edge cases, use the official reference from MDN:
That’s the real rulebook. Your browser console is the lie detector.