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.
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:
typeof PaystackPop and press Enter"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:
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();
});
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
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;
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:
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.
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.
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:
What you can do about it:
// 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.
Work through this list when the popup does not open:
typeof PaystackPop in console. Should return "function". If "undefined", the script did not load.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.
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.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course