Paystack Popup Not Opening: Frontend Debug Guide
If the Paystack popup does not open, check these in order: (1) The Paystack Inline JS script is loaded and PaystackPop is defined. (2) The popup is triggered by a direct user action (click handler), not by a timer or async callback, because browsers block popups not initiated by user gestures. (3) Your public key is correct and matches your environment (test vs live). (4) Your Content Security Policy allows frames and scripts from paystack.com. Open your browser console and look for errors. The cause will be visible there.
Check 1: Is the Paystack Script Loaded?
The Paystack Inline JS popup requires the Paystack script to be loaded in your page. If it is missing or failed to load, nothing happens when you try to open the popup.
The correct script tag (V2):
<script src="https://js.paystack.co/v2/inline.js"></script>
The old script tag (V1, still works but deprecated):
<script src="https://js.paystack.co/v1/inline.js"></script>
How to verify the script loaded:
- Open your browser DevTools (F12 or Cmd+Option+I)
- Go to the Console tab
- Type
typeof PaystackPopand press Enter - If it returns
"function", the script is loaded. If it returns"undefined", the script did not load.
If the script did not load, check the Network tab in DevTools. Filter by "paystack" and look for the script request. Common reasons it fails:
- Script tag has a typo in the URL
- Script tag is missing from the page entirely
- An ad blocker or browser extension blocked the request
- A Content Security Policy blocked the script
- Network error (no internet, DNS failure)
Check 2: Browser Popup Blocker
Modern browsers block popups and new windows that are not triggered by a direct user action. "Direct user action" means a click or keypress event handler that runs synchronously.
These work (direct user action):
// Click handler directly opens the popup
document.getElementById('pay-btn').addEventListener('click', function() {
const handler = PaystackPop.setup({
key: 'pk_test_xxxxx',
email: 'customer@email.com',
amount: 50000,
onClose: function() {
console.log('Popup closed');
},
callback: function(response) {
console.log('Payment complete:', response.reference);
},
});
handler.openIframe();
});
These may be blocked (not a direct user action):
// RISKY: Opening after an async operation
document.getElementById('pay-btn').addEventListener('click', async function() {
// The fetch takes time. By the time it resolves,
// the browser no longer considers this a "user action."
const response = await fetch('/api/create-transaction');
const data = await response.json();
// This may be blocked by the popup blocker
const handler = PaystackPop.setup({
key: 'pk_test_xxxxx',
email: data.email,
amount: data.amount,
});
handler.openIframe(); // Browser may block this
});
// BLOCKED: Opening on a timer
setTimeout(function() {
const handler = PaystackPop.setup({ /* ... */ });
handler.openIframe(); // Will almost certainly be blocked
}, 3000);
The fix for async scenarios: Initialize the transaction on the server before the user clicks the button. When the user clicks, you already have all the data and can open the popup synchronously.
// Prepare data BEFORE the click
let paymentData = null;
async function preparePayment() {
const response = await fetch('/api/prepare-payment');
paymentData = await response.json();
}
// Call this when the page loads or when the user fills out the form
preparePayment();
// On click, open immediately with pre-fetched data
document.getElementById('pay-btn').addEventListener('click', function() {
if (!paymentData) {
alert('Still loading payment details. Try again.');
return;
}
const handler = PaystackPop.setup({
key: paymentData.publicKey,
email: paymentData.email,
amount: paymentData.amount,
ref: paymentData.reference,
callback: function(response) {
console.log('Payment complete:', response.reference);
},
});
handler.openIframe();
});
Check 3: Public Key Is Correct
Paystack Inline JS requires your public key, not your secret key. The public key starts with pk_test_ (test mode) or pk_live_ (live mode).
Common key mistakes:
// WRONG: Using secret key on the frontend
key: 'sk_test_xxxxx' // NEVER expose your secret key in frontend code
// WRONG: Using an empty or undefined key
key: ''
key: undefined
key: process.env.PAYSTACK_PUBLIC_KEY // This is server-side, not available in browser
// WRONG: Test key in production
key: 'pk_test_xxxxx' // Works in test mode, but live payments will fail
// RIGHT: Public key
key: 'pk_live_xxxxx' // or pk_test_ for testing
How to verify:
// Add this before PaystackPop.setup()
const key = 'pk_test_xxxxx'; // your key
console.log('Key prefix:', key.substring(0, 8));
console.log('Key length:', key.length);
// pk_test_ = 8 chars prefix + key = should be a reasonable length
// If key is empty, undefined, or starts with sk_, that is the problem
If you are using a framework like React or Vue, make sure the environment variable is available in the browser. In React (Create React App), prefix it with REACT_APP_. In Next.js, prefix with NEXT_PUBLIC_. In Vite, prefix with VITE_.
// Next.js: must start with NEXT_PUBLIC_
// .env.local
NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY=pk_test_xxxxx
// In your component
const key = process.env.NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY;
Check 4: Event Handler Is Attached
If clicking the pay button does nothing at all (no errors in console, no popup), the event handler may not be attached to the button.
Common causes:
- The script runs before the DOM is ready, so the button element does not exist yet
- The button ID or selector is wrong
- Another element is overlaying the button (z-index issue)
- The button is inside a form that submits and reloads the page before the handler runs
How to verify:
// Add this and click the button
document.getElementById('pay-btn').addEventListener('click', function(e) {
console.log('Button clicked!'); // Does this appear in console?
// If using a form, prevent default submission
e.preventDefault();
// Your Paystack code...
});
// If "Button clicked!" does NOT appear:
// 1. Check the element ID: document.getElementById('pay-btn') === null?
// 2. Check DOM readiness: wrap in DOMContentLoaded
document.addEventListener('DOMContentLoaded', function() {
const btn = document.getElementById('pay-btn');
if (!btn) {
console.error('Pay button not found!');
return;
}
btn.addEventListener('click', function(e) {
e.preventDefault();
console.log('Handler attached and button clicked');
// Paystack code...
});
});
If the button is inside a <form>, the form's default submit action will fire and may reload the page before your Paystack code runs. Always call e.preventDefault() at the start of your click handler.
Check 5: Content Security Policy Blocking
If your site has a Content Security Policy (CSP), it may block the Paystack script or iframe. The browser console will show a CSP violation error.
What CSP violations look like in the console:
Refused to load the script 'https://js.paystack.co/v2/inline.js' because it
violates the following Content Security Policy directive: "script-src 'self'"
Refused to frame 'https://checkout.paystack.com/' because it violates the
following Content Security Policy directive: "frame-src 'self'"
The fix: Update your CSP headers to allow Paystack domains:
Content-Security-Policy:
script-src 'self' https://js.paystack.co;
frame-src 'self' https://checkout.paystack.com;
connect-src 'self' https://api.paystack.co;
In an Express app:
// Using helmet.js
const helmet = require('helmet');
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "https://js.paystack.co"],
frameSrc: ["'self'", "https://checkout.paystack.com"],
connectSrc: ["'self'", "https://api.paystack.co"],
},
})
);
In a Next.js app (next.config.js):
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: [
"default-src 'self'",
"script-src 'self' https://js.paystack.co",
"frame-src 'self' https://checkout.paystack.com",
"connect-src 'self' https://api.paystack.co",
].join('; '),
},
];
module.exports = {
async headers() {
return [
{
source: '/(.*)',
headers: securityHeaders,
},
];
},
};
If you are not sure whether CSP is the issue, temporarily remove the CSP header and test. If the popup works without CSP, you know you need to update the policy.
Check 6: Ad Blockers and Browser Extensions
Ad blockers, privacy extensions (like uBlock Origin, Privacy Badger, or Brave Shields), and some antivirus software can block the Paystack script. They see a third-party payment script and flag it as a tracker or ad.
How to verify:
- Open your page in an incognito/private window with all extensions disabled
- If the popup works in incognito but not in your normal browser, an extension is the cause
- Disable extensions one by one to find the culprit
What you can do about it:
- You cannot force users to disable their ad blockers. This is a reality of online payments.
- Provide a fallback: if the Paystack popup fails, redirect the user to the Paystack hosted checkout page instead. The redirect method works even when the Inline JS script is blocked.
- Detect if the script loaded and show a helpful message if it did not.
// Detect if Paystack loaded and offer fallback
function initiatePayment() {
if (typeof PaystackPop === 'undefined') {
// Script did not load. Redirect to hosted checkout instead.
console.warn('Paystack Inline JS not available. Using redirect checkout.');
window.location.href = '/api/paystack/checkout-redirect';
return;
}
// Inline JS is available, use the popup
const handler = PaystackPop.setup({
key: 'pk_live_xxxxx',
email: 'customer@email.com',
amount: 50000,
callback: function(response) {
window.location.href = '/payment/verify?ref=' + response.reference;
},
});
handler.openIframe();
}
The redirect fallback uses server-side initialization and the Paystack-hosted checkout page, which is not affected by client-side script blockers.
Complete Debug Checklist
Work through this list when the popup does not open:
- Open browser console (F12). Are there any errors? CSP violations? Script loading failures?
- Check Network tab. Did the Paystack script load? Filter by "paystack".
- Type
typeof PaystackPopin console. Should return "function". If "undefined", the script did not load. - Check your public key. Starts with pk_test_ or pk_live_? Not empty? Not the secret key?
- Check the click handler. Add a console.log at the start. Does it fire?
- Check for form submission. Is e.preventDefault() called? Is the page reloading?
- Check for async issues. Is the popup opened inside a fetch/await/setTimeout? Try opening it synchronously.
- Test in incognito. Does it work with extensions disabled?
- Check CSP headers. Do you have a Content-Security-Policy that blocks paystack.com?
- Check for z-index issues. Is another element covering the button or the popup iframe?
For the complete error reference, see Paystack Errors and Troubleshooting: The Complete Reference. If the script itself fails to load, see Paystack Inline Script Not Loading.
React-Specific Issues
If you are using React, there are additional considerations:
Loading the script in React:
// Option 1: Add to public/index.html (CRA) or app/layout.tsx (Next.js)
// In public/index.html:
<script src="https://js.paystack.co/v2/inline.js"></script>
// Option 2: Load dynamically in a useEffect
import { useEffect, useState } from 'react';
function usePaystack() {
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (typeof PaystackPop !== 'undefined') {
setLoaded(true);
return;
}
const script = document.createElement('script');
script.src = 'https://js.paystack.co/v2/inline.js';
script.async = true;
script.onload = () => setLoaded(true);
script.onerror = () => console.error('Failed to load Paystack script');
document.head.appendChild(script);
return () => {
document.head.removeChild(script);
};
}, []);
return loaded;
}
// In your component
function PayButton({ email, amount }) {
const paystackLoaded = usePaystack();
const handleClick = () => {
if (!paystackLoaded) {
alert('Payment system is loading. Please try again.');
return;
}
const handler = PaystackPop.setup({
key: process.env.NEXT_PUBLIC_PAYSTACK_PUBLIC_KEY,
email,
amount,
callback: (response) => {
console.log('Payment complete:', response.reference);
},
});
handler.openIframe();
};
return (
<button onClick={handleClick} disabled={!paystackLoaded}>
{paystackLoaded ? 'Pay Now' : 'Loading...'}
</button>
);
}
The key point: make sure PaystackPop is available before the user clicks. If the script has not loaded yet, disable the button or show a loading state.
Key Takeaways
- ✓The most common cause is the Paystack Inline JS script not being loaded. Check that the script tag is present, uses the correct URL, and has loaded before your code runs.
- ✓Browsers block popups that are not triggered by a direct user action. If you call PaystackPop.setup().openIframe() inside a setTimeout, fetch callback, or promise chain, the browser may block it.
- ✓PaystackPop is a global variable set by the Paystack Inline JS script. If it is undefined, the script has not loaded. Check the network tab for loading errors.
- ✓The public key (pk_test_xxx or pk_live_xxx) is required. If it is wrong, empty, or a secret key, the popup will fail silently or show an error inside the iframe.
- ✓Content Security Policy (CSP) headers can block the Paystack iframe. You need to allow frame-src and script-src for paystack.com domains.
- ✓Always check the browser console first. Paystack errors, CSP violations, and script loading failures all appear there.
- ✓Ad blockers and privacy extensions can block the Paystack script. Test in an incognito window with extensions disabled to rule this out.
Frequently Asked Questions
- Why does the Paystack popup work in development but not in production?
- The most common causes are: (1) You are using a test public key in production. Switch to pk_live_xxx. (2) Your production site has a Content Security Policy that blocks Paystack domains. (3) Your production CDN or hosting provider adds headers that block third-party scripts. Check the browser console in production for the specific error.
- Can I use Paystack Inline JS in a React Native or mobile app?
- Paystack Inline JS is designed for web browsers. For mobile apps, use the Paystack Mobile SDKs (iOS and Android) or embed a WebView that loads your Paystack checkout page. Do not try to load the Inline JS script in a native mobile context.
- Does the popup work in all browsers?
- Paystack Inline JS works in all modern browsers: Chrome, Firefox, Safari, Edge, and their mobile versions. It does not work in Internet Explorer. Very old browser versions may have issues. If a customer reports the popup not working, ask them to try a different browser or update their current one.
- How do I handle the case where the popup is blocked?
- Detect whether PaystackPop is available. If it is not (script blocked) or if the popup fails to open (browser blocked), fall back to the redirect checkout method. Initialize the transaction on your server, get the authorization_url from Paystack, and redirect the user to that URL.
- Should I use Paystack Inline JS V1 or V2?
- Use V2 (https://js.paystack.co/v2/inline.js). V1 is older and Paystack recommends V2 for new integrations. If you are on V1, see the Paystack migration guide for differences. The main changes are in how you initialize the popup and handle callbacks.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon