Google Pay is one of those integrations that looks tiny in code and then immediately punches a hole through your CSP if you guessed the source list wrong.
You add a payment button, load the Google Pay JavaScript, open a payment sheet, and suddenly the browser starts yelling about blocked scripts, frames, or network connections. I’ve dealt with this enough times that I now treat payment integrations as CSP work first and feature work second.
This guide is the practical version: what to allow, what to avoid, and copy-paste policies you can start from.
What Google Pay usually needs in CSP
A browser-based Google Pay integration commonly needs these CSP directives adjusted:
script-srcfor the Google Pay JavaScriptconnect-srcfor API calls made by the integrationframe-srcbecause payment flows often use embedded frames or popups under the hood- sometimes
img-srcif branded assets are loaded remotely
If you use a strict CSP with nonces and strict-dynamic, you need to think a little differently about third-party scripts. If you use a host allowlist policy, you need to explicitly include Google Pay origins.
Minimal CSP for Google Pay
Here’s a minimal starter policy for a site that only needs Google Pay and nothing fancy:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://pay.google.com;
connect-src 'self' https://pay.google.com;
frame-src https://pay.google.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
This is not my favorite style long term, but it’s a clean baseline when you’re trying to get an integration working.
A more realistic CSP
Most production sites already have analytics, tag managers, cookie consent, and a stricter policy. You gave a real-world header from headertest.com, which is a good example of how messy CSP gets once real business requirements show up.
Here’s that policy with Google Pay added in the places that usually matter:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-YjIwMGQ1OTgtMzk5ZC00YmU0LTljY2UtNjUwZTA4ODA1MjBk' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://pay.google.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://app.tallytics.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com https://pay.google.com;
frame-src 'self' https://consentcdn.cookiebot.com https://pay.google.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
That’s the “get it in place and test” version.
A thing I’d call out: if you use 'strict-dynamic', host allowlists in script-src become less relevant in supporting browsers once a nonce- or hash-trusted script runs. That can be good, but it also confuses teams because they keep adding hosts and don’t understand why behavior doesn’t change. If your app relies on nonce-based bootstrapping, make sure the Google Pay loader is introduced by a trusted script path.
If you want a deeper breakdown of directives like script-src, connect-src, or frame-src, https://csp-guide.com is a good reference.
Copy-paste example: Google Pay with a nonce-based CSP
If your app already uses nonces, I’d structure it like this:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://pay.google.com;
connect-src 'self' https://pay.google.com;
frame-src 'self' https://pay.google.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Example HTML:
<script nonce="{{nonce}}" src="/static/app.js"></script>
Then in your trusted app bundle:
const script = document.createElement('script');
script.src = 'https://pay.google.com/gp/p/js/pay.js';
script.async = true;
document.head.appendChild(script);
That pattern works well because your nonced app script is allowed to load the Google Pay library.
Copy-paste example: classic host allowlist CSP
If you don’t use nonces yet, this is the simpler version:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://pay.google.com;
connect-src 'self' https://pay.google.com;
frame-src 'self' https://pay.google.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
And the HTML:
<script async src="https://pay.google.com/gp/p/js/pay.js"></script>
Example Google Pay button code
Here’s a basic client-side setup you can actually wire into a test page:
<button id="google-pay-button">Pay with Google Pay</button>
<script async src="https://pay.google.com/gp/p/js/pay.js"></script>
<script>
const paymentsClient = new google.payments.api.PaymentsClient({
environment: 'TEST'
});
const paymentDataRequest = {
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
allowedCardNetworks: ['VISA', 'MASTERCARD']
},
tokenizationSpecification: {
type: 'PAYMENT_GATEWAY',
parameters: {
gateway: 'example',
gatewayMerchantId: 'exampleMerchantId'
}
}
}],
merchantInfo: {
merchantName: 'Demo Merchant'
},
transactionInfo: {
totalPriceStatus: 'FINAL',
totalPrice: '10.00',
currencyCode: 'USD',
countryCode: 'US'
}
};
document.getElementById('google-pay-button').addEventListener('click', async () => {
try {
const paymentData = await paymentsClient.loadPaymentData(paymentDataRequest);
console.log('Google Pay payment data:', paymentData);
} catch (err) {
console.error('Google Pay failed:', err);
}
});
</script>
If this gets blocked, DevTools will usually tell you exactly which directive failed.
Common CSP mistakes with Google Pay
1. Allowing script-src but forgetting frame-src
This is the classic one.
The script loads fine, the button renders, then the payment UI fails because the browser blocks a frame. If you only update script-src, you’re only halfway done.
Use:
frame-src 'self' https://pay.google.com;
2. Forgetting connect-src
Even when the script and frame load, API calls can still fail.
Use:
connect-src 'self' https://pay.google.com;
If your payment flow also talks to your own backend or gateway endpoints, include those too.
3. Using default-src and assuming it covers everything
It does as a fallback, but real policies usually override script-src, connect-src, and frame-src. Once you define those, default-src stops helping for those resource types.
I still see teams add a domain to default-src and wonder why the violation persists.
4. Overusing wildcards
Don’t do this unless you absolutely have to:
script-src 'self' https://*.google.com https://*.gstatic.com;
That’s broader than Google Pay needs and weakens the policy for no real benefit. Start narrow and expand only when you have a concrete blocked URL in a report.
Report-Only rollout
If you’re adding Google Pay to an existing app, use Content-Security-Policy-Report-Only first. That lets you see what would break before you ship the enforcement header.
Example:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://pay.google.com;
connect-src 'self' https://pay.google.com;
frame-src 'self' https://pay.google.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
If you collect reports, make sure you validate them against actual browser behavior. CSP reports are useful, but they can get noisy fast.
Recommended production template
If you want one sane starting point, I’d use this and then tailor it to your app:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM_NONCE}' 'strict-dynamic' https://pay.google.com;
connect-src 'self' https://pay.google.com https://your-api.example.com;
frame-src 'self' https://pay.google.com;
img-src 'self' data: https:;
style-src 'self' 'unsafe-inline';
font-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
report-to default-endpoint;
If you don’t have nonce support, replace script-src with:
script-src 'self' https://pay.google.com;
Sanity checklist
Before calling your CSP “done”, check these:
- Google Pay script loads without CSP violations
- payment sheet opens successfully
- no
connect-srcviolations during tokenization or payment data loading - no
frame-srcviolations when the UI appears - reports are clean in both test and production-like environments
- your policy does not broaden unrelated Google domains without evidence
For Google Pay-specific integration behavior, the official docs are still the source of truth: https://developers.google.com/pay
For CSP behavior and directive details, the official CSP spec and browser docs help when the browser error message is vague. But in practice, most fixes come down to this: watch DevTools, add the exact origin you need, and resist the temptation to slap https: everywhere just to make the error go away.
That shortcut always comes back to bite you.