Adyen is one of those integrations where CSP gets real fast.

A normal marketing site can get away with a basic policy and a couple of allowlists. Payments are different. You’re loading third-party scripts, embedding frames, sending XHR requests to payment endpoints, and sometimes dealing with redirects or 3D Secure flows. If your CSP is too strict, checkout breaks. Too loose, and you’ve basically given up the point of having CSP.

I’ve had the best results by treating payment pages as their own security boundary. Don’t try to reuse the exact same CSP from your homepage or blog. Build a payment-specific policy.

What Adyen needs from CSP

For a typical Adyen Web integration, CSP usually needs to allow:

  • Adyen JavaScript
  • Adyen styles
  • Adyen frames for card fields and challenge flows
  • Adyen API calls
  • Images and assets used by Adyen UI
  • Your own backend endpoints for /payments, /payments/details, or sessions

The exact domains can vary by:

  • test vs live
  • region
  • Drop-in vs Components
  • features like Apple Pay, Google Pay, PayPal, 3D Secure

That’s why hardcoding a giant permissive policy is a bad habit. Start with the Adyen features you actually use, then expand only when the browser reports a violation.

For official references, use Adyen’s documentation and browser CSP references from official docs. If you want a ready-to-use baseline pattern, https://csp-examples.com is useful for policy structure.

Start from a sane baseline

Here’s the real CSP header from headertest.com that you provided:

content-security-policy: default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com; script-src 'self' 'nonce-YmZkODljYWItY2ZiNC00Y2U2LTg4MjEtY2VhOGY1NDA2N2Y1' '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’s a decent marketing-site CSP, but it will not support Adyen checkout by itself. Missing pieces usually include:

  • Adyen script origins in script-src
  • Adyen stylesheet origins in style-src
  • Adyen API endpoints in connect-src
  • Adyen hosted frames in frame-src
  • sometimes broader img-src for payment logos or challenge assets

A practical Adyen CSP

Here’s a good starting point for an Adyen payment page. I’m keeping it focused and reasonably strict.

Content-Security-Policy:
  default-src 'self';
  base-uri 'self';
  object-src 'none';
  frame-ancestors 'none';
  form-action 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;
  style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
  img-src 'self' data: https:;
  font-src 'self' https://checkoutshopper-test.adyen.com;
  connect-src 'self' https://checkoutshopper-test.adyen.com https://{your-api-origin};
  frame-src 'self' https://checkoutshopper-test.adyen.com;
  report-to default-endpoint;
  report-uri /csp-report;

For live, you’ll replace the test host with the live Adyen host used by your account and region.

A few opinions here:

  • object-src 'none' should always be there.
  • base-uri 'self' is cheap protection. Use it.
  • frame-ancestors 'none' is right for standalone checkout pages unless your app must be embedded.
  • I’m fine with 'unsafe-inline' in style-src on payment pages if the library forces the issue. I’m not fine with it in script-src.
  • Use nonces for your own inline bootstrapping code instead of allowing inline scripts globally.

Example HTML with nonce-based bootstrapping

This is the pattern I’d use for Adyen Web Drop-in.

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Checkout</title>
  <link rel="stylesheet"
        href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.css">
</head>
<body>
  <div id="payment"></div>

  <script nonce="{{cspNonce}}"
          src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.js"></script>

  <script nonce="{{cspNonce}}">
    async function startCheckout() {
      const sessionResponse = await fetch('/api/adyen/sessions', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        credentials: 'same-origin'
      });

      const session = await sessionResponse.json();

      const checkout = await AdyenCheckout({
        environment: 'test',
        clientKey: '{{ADYEN_CLIENT_KEY}}',
        session,
        onPaymentCompleted: (result) => {
          console.log('Payment completed', result);
          window.location.href = '/checkout/success';
        },
        onError: (error) => {
          console.error(error);
        }
      });

      checkout.create('dropin').mount('#payment');
    }

    startCheckout();
  </script>
</body>
</html>

If you don’t use nonces and rely on 'unsafe-inline' for scripts, you’re weakening the page that handles payment setup. That’s exactly the page I’d keep the tightest.

Express example: generate and send the CSP header

Here’s a simple Node/Express setup that generates a nonce per request.

import express from 'express';
import crypto from 'crypto';

const app = express();

function buildCsp(nonce) {
  return [
    "default-src 'self'",
    "base-uri 'self'",
    "object-src 'none'",
    "frame-ancestors 'none'",
    "form-action 'self'",
    `script-src 'self' 'nonce-${nonce}' 'strict-dynamic' https://checkoutshopper-test.adyen.com`,
    "style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com",
    "img-src 'self' data: https:",
    "font-src 'self' https://checkoutshopper-test.adyen.com",
    "connect-src 'self' https://checkoutshopper-test.adyen.com",
    "frame-src 'self' https://checkoutshopper-test.adyen.com",
    "report-uri /csp-report"
  ].join('; ');
}

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');
  res.locals.cspNonce = nonce;
  res.setHeader('Content-Security-Policy', buildCsp(nonce));
  next();
});

app.get('/checkout', (req, res) => {
  res.send(`
    <!doctype html>
    <html>
      <head>
        <link rel="stylesheet" href="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.css">
      </head>
      <body>
        <div id="payment"></div>
        <script nonce="${res.locals.cspNonce}" src="https://checkoutshopper-test.adyen.com/checkoutshopper/sdk/6.0.0/adyen.js"></script>
        <script nonce="${res.locals.cspNonce}">
          console.log('Boot Adyen here');
        </script>
      </body>
    </html>
  `);
});

app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/reports+json'] }), (req, res) => {
  console.log('CSP violation:', JSON.stringify(req.body, null, 2));
  res.sendStatus(204);
});

app.listen(3000);

Extending your existing site policy

If your current site policy looks like the headertest.com example, don’t dump Adyen domains into the global policy unless checkout runs everywhere.

Make a route-specific CSP for /checkout and related payment pages.

For example, your existing policy has:

script-src 'self' 'nonce-YmZkODljYWItY2ZiNC00Y2U2LTg4MjEtY2VhOGY1NDA2N2Y1' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;

For checkout, evolve it like this:

script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://checkoutshopper-test.adyen.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com https://checkoutshopper-test.adyen.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://checkoutshopper-test.adyen.com https://your-backend.example;
frame-src 'self' https://consentcdn.cookiebot.com https://checkoutshopper-test.adyen.com;
font-src 'self' https://checkoutshopper-test.adyen.com;

That’s much better than widening default-src and hoping everything works.

Common breakages

1. Card fields or 3DS challenge never render

Usually frame-src is missing the Adyen origin.

Check browser console errors like:

Refused to frame 'https://checkoutshopper-test.adyen.com/' because it violates the following Content Security Policy directive: "frame-src 'self'".

Fix:

frame-src 'self' https://checkoutshopper-test.adyen.com;

2. Payment methods fail to load

Usually connect-src is too narrow.

Adyen’s frontend makes network requests. If those are blocked, the UI may show a spinner forever.

Fix:

connect-src 'self' https://checkoutshopper-test.adyen.com https://your-api.example;

3. Adyen JS loads, but CSS or icons look broken

You probably forgot style-src, font-src, or a broad enough img-src.

Fix:

style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
font-src 'self' https://checkoutshopper-test.adyen.com;
img-src 'self' data: https:;

4. Your own inline setup script is blocked

Good. That means CSP is doing its job.

Use a nonce:

script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;

Then add the same nonce to the script tag.

Report-only first, then enforce

For payment flows, I like to deploy in two steps:

  1. Content-Security-Policy-Report-Only
  2. real Content-Security-Policy

That gives you time to catch weird edge cases like wallet flows, challenge windows, or region-specific endpoints before customers hit them.

Example:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://checkoutshopper-test.adyen.com;
  style-src 'self' 'unsafe-inline' https://checkoutshopper-test.adyen.com;
  connect-src 'self' https://checkoutshopper-test.adyen.com https://your-api-origin;
  frame-src 'self' https://checkoutshopper-test.adyen.com;
  img-src 'self' data: https:;
  font-src 'self' https://checkoutshopper-test.adyen.com;
  report-uri /csp-report;

Keep checkout isolated

If there’s one thing I’d push hard, it’s this: isolate your payment CSP from the rest of your app.

Checkout pages should have:

  • fewer analytics tags
  • fewer third-party scripts
  • fewer experiments
  • fewer widgets
  • a tighter CSP than the rest of the site

Every extra origin in script-src, connect-src, or frame-src is another thing that can break or be abused.

For official implementation details, check Adyen’s own documentation for the specific Web version and payment methods you use, because the exact endpoints and asset hosts can differ by setup. That part matters.

A practical rollout looks like this:

  • build a dedicated checkout CSP
  • add only the Adyen origins you actually need
  • use nonces for inline bootstrapping
  • test 3DS and alternative payment methods
  • run report-only first
  • enforce once violations are clean

That approach is boring, strict, and reliable. For payments, boring and reliable wins every time.