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-src or form-action is 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.

For ConvertKit forms, these directives are the ones you’ll most likely touch:

  • script-src
  • connect-src
  • style-src
  • img-src
  • frame-src
  • form-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.com because ConvertKit may serve assets or form endpoints from subdomains.
  • I included 'unsafe-inline' for script-src and style-src because 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: in img-src with explicit hosts if you can
  • replace 'unsafe-inline' in script-src with nonces
  • keep object-src 'none'
  • keep base-uri 'self'
  • keep form-action explicit instead of relying on default-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-uri or report-to endpoint 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.