McTaba Labs logo
Software EngineeringThe WhiteboardProjectsEventsMasterclassesWebinarsLearn
Book Call

500+ developers already in the community

Get answers, find accountability partners, and stay ahead

Join Discord →
McTaba Labs logo

Educating 1 million students in tech by 2030.

Programs

  • HighSchool Graduates Launchpad
  • AI & Business Automation
  • Corporate Training
  • McTaba Story & Mission
  • Full-Stack Curriculum
  • Course Pricing & Plans
  • Frequently Asked Questions

Resources

  • Join Discord Community
  • Tech Events in Nairobi
  • Masterclasses
  • Free Resources
  • Learn
  • Campus
  • Africa Tech Talent Report
  • Partner With Us
  • Start a Project
  • Our Work

Contact Us

  • 124 Manyani East Road, Lavington, Nairobi
  • +254727795603
  • Book a Discovery Call

Office Hours:

Mon-Fri: 9am - 2pm EAT

Sat: 10am - 12pm EAT

© 2027 McTaba Labs - A product of Edtech Industries. All rights reserved.

Privacy PolicyTerms of ServiceRefund Policy
  1. Learn
  2. Paystack for African Developers
  3. Paystack Callback URL Not Redirecting
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack Callback URL Not Redirecting

Paystack does not redirect customers after payment for three main reasons: you did not set a callback_url when initializing the transaction, you are using the popup (Inline.js) mode which uses a JavaScript callback instead of a URL redirect, or the browser is blocking the redirect due to popup blockers or CSP rules. For redirect mode, include callback_url in your /transaction/initialize request. For popup mode, handle the redirect yourself in the callback function. If you set a callback_url in the dashboard but not in the API call, the API call takes precedence.

In this article

  1. 1. What "Not Redirecting" Looks Like
  2. 2. Cause 1: callback_url Not Set (Redirect Mode)
  3. 3. Cause 2: Using Popup Mode Without Manual Redirect
  4. 4. Cause 3: Browser Blocking the Popup or Redirect
  5. 5. Setting Up Your Callback Page
  6. 6. Dashboard Callback URL vs API Callback URL
  7. 7. When to Use Redirect Mode vs Popup Mode
  8. 8. Verification: Confirm Redirects Work

What "Not Redirecting" Looks Like

After a successful payment, the customer expects to land back on your site (a thank-you page, a receipt, a dashboard). Instead, one of these happens:

  • The customer stays on the Paystack checkout page with a "Payment Successful" message but no way to return to your site
  • The Paystack popup closes but nothing happens on your page
  • The customer sees a blank page or a "this page can't be found" error at the callback URL
  • The redirect works for you during testing but fails for some customers

The root cause depends on which Paystack checkout mode you are using. Let's look at both.

Cause 1: callback_url Not Set (Redirect Mode)

In redirect mode, you initialize a transaction via the API, get an authorization_url, and send the customer to that URL. After payment, Paystack redirects the customer to your callback_url.

The problem: if you do not include callback_url in your API request, Paystack does not know where to send the customer back.

// BROKEN: no callback_url
const response = await fetch('https://api.paystack.co/transaction/initialize', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'customer@example.com',
    amount: 500000,  // 5,000 NGN in kobo
    // callback_url is missing!
  }),
});

The fix: Add callback_url to your initialization request.

// FIXED: callback_url included
const response = await fetch('https://api.paystack.co/transaction/initialize', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'customer@example.com',
    amount: 500000,
    callback_url: 'https://yoursite.com/payment/verify',
    // Use your production domain, not localhost
  }),
});

const data = await response.json();
// Redirect the customer to data.data.authorization_url
// After payment, Paystack redirects to:
// https://yoursite.com/payment/verify?trxref=abc123&reference=abc123

You can also set a default callback URL in your Paystack dashboard under Settings > API Keys & Webhooks. But the URL in the API request always takes priority. If you set different URLs in the dashboard and the API request, the API request wins.

Important: The callback_url must be a full URL with the protocol. yoursite.com/payment/verify will not work. It must be https://yoursite.com/payment/verify.

Get weekly developer tips

Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.

No spam. Unsubscribe anytime.

Cause 2: Using Popup Mode Without Manual Redirect

In popup mode (Inline.js / PaystackPop), there is no automatic redirect. The popup opens, the customer pays, the popup closes, and your callback function runs. If you do not redirect the customer in that function, they stay on the same page.

// BROKEN: callback function does nothing useful
const handler = PaystackPop.setup({
  key: 'pk_test_xxxxx',
  email: 'customer@example.com',
  amount: 500000,
  callback: function(response) {
    console.log('Payment complete:', response.reference);
    // Customer stays on the same page. No redirect.
  },
  onClose: function() {
    console.log('Popup closed');
  },
});
handler.openIframe();

The fix: Redirect the customer in the callback function.

// FIXED: redirect after payment
const handler = PaystackPop.setup({
  key: 'pk_test_xxxxx',
  email: 'customer@example.com',
  amount: 500000,
  callback: function(response) {
    // Redirect to your verification/thank-you page
    window.location.href = `/payment/verify?reference=${response.reference}`;
  },
  onClose: function() {
    // Customer closed the popup without completing payment
    alert('Payment was not completed. Please try again.');
  },
});
handler.openIframe();

In React or Next.js, use the router instead of window.location:

// React with react-router
import { useNavigate } from 'react-router-dom';

function CheckoutButton() {
  const navigate = useNavigate();

  const handlePayment = () => {
    const handler = PaystackPop.setup({
      key: 'pk_test_xxxxx',
      email: 'customer@example.com',
      amount: 500000,
      callback: (response) => {
        navigate(`/payment/verify?reference=${response.reference}`);
      },
      onClose: () => {
        // Optionally show a message
      },
    });
    handler.openIframe();
  };

  return <button onClick={handlePayment}>Pay Now</button>;
}

The callback function receives an object with a reference property. Pass this reference to your server to verify the payment.

Cause 3: Browser Blocking the Popup or Redirect

Even with the correct code, browsers can block the popup or redirect in several ways:

Popup blockers: If you open the Paystack popup from a timer, an async callback, or anything that is not a direct user click, the browser treats it as a popup and blocks it. The popup never opens, so the callback never fires, and no redirect happens.

// BROKEN: popup opened from setTimeout, not a direct click
setTimeout(() => {
  const handler = PaystackPop.setup({ /* ... */ });
  handler.openIframe();  // Browser may block this
}, 1000);

// BROKEN: popup opened from an async chain
async function pay() {
  const userData = await fetchUserData();  // async operation
  const handler = PaystackPop.setup({
    email: userData.email,
    // ...
  });
  handler.openIframe();  // May be blocked because it is not a direct click
}

The fix: Always open the popup directly from a click handler. If you need to fetch data first, fetch it before the click or show a loading state.

// FIXED: popup opens directly from click
function handleClick() {
  // All setup is synchronous from the click event
  const handler = PaystackPop.setup({
    key: 'pk_test_xxxxx',
    email: document.getElementById('email').value,
    amount: 500000,
    callback: (response) => {
      window.location.href = `/payment/verify?reference=${response.reference}`;
    },
  });
  handler.openIframe();  // Direct from click, browser allows it
}

document.getElementById('pay-button').addEventListener('click', handleClick);

CSP frame-src blocking: If your Content Security Policy blocks standard.paystack.co in the frame-src directive, the popup iframe will not load. See Paystack Checkout Blocked by Content Security Policy for the fix.

Browser extensions: Some ad blockers and privacy extensions block third-party iframes. There is not much you can do about this except to offer redirect mode as a fallback. Redirect mode opens a full Paystack page instead of an iframe, so extensions are less likely to block it.

Setting Up Your Callback Page

When Paystack redirects to your callback URL, it appends the transaction reference as query parameters. Your callback page must read these parameters and verify the payment server-side.

// Example callback page: /payment/verify
// Server-side handler (Express)

router.get('/payment/verify', async (req, res) => {
  const reference = req.query.reference || req.query.trxref;

  if (!reference) {
    return res.status(400).send('No payment reference provided');
  }

  // Verify the transaction with Paystack
  const paystackResponse = await fetch(
    `https://api.paystack.co/transaction/verify/${reference}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
      },
    }
  );

  const data = await paystackResponse.json();

  if (data.data.status === 'success') {
    // Payment confirmed. Grant access, send receipt, update database.
    const amountPaid = data.data.amount / 100;  // Convert from kobo
    const customerEmail = data.data.customer.email;

    // Your business logic here
    await grantAccess(customerEmail);

    res.render('payment-success', {
      amount: amountPaid,
      reference: reference,
    });
  } else {
    // Payment failed or is still pending
    res.render('payment-failed', {
      status: data.data.status,
      message: data.data.gateway_response,
    });
  }
});

Critical: never trust the redirect alone. A customer could manually type your callback URL into their browser without paying. Always verify the transaction reference with Paystack's API before granting access.

Common callback URL mistakes:

  • Using localhost in production: http://localhost:3000/payment/verify will not work for real users
  • Forgetting the protocol: yoursite.com/verify is not a valid URL. Use https://yoursite.com/verify
  • Trailing slash mismatch: if your server treats /verify and /verify/ differently, make sure the callback URL matches
  • Path does not exist: the callback URL must point to an actual route on your server. A 404 page means the route is not set up

Dashboard Callback URL vs API Callback URL

Paystack lets you set a callback URL in two places:

  1. Dashboard: Settings > API Keys & Webhooks > Callback URL. This is the default for all transactions that do not specify a callback_url in the API request.
  2. API request: The callback_url field in the /transaction/initialize request body. This overrides the dashboard setting for that specific transaction.

Priority: API request > Dashboard setting. If both are set, the API request wins.

This can cause confusion. If your dashboard has https://yoursite.com/old-callback and your API sends https://yoursite.com/new-callback, the customer goes to /new-callback. If your API does not send a callback_url, the customer goes to /old-callback.

Recommendation: set the callback_url in every API request. Do not rely on the dashboard setting. This makes your code self-documenting and prevents surprises when someone changes the dashboard setting.

// Set callback_url explicitly in every request
const callbackUrl = process.env.NODE_ENV === 'production'
  ? 'https://yoursite.com/payment/verify'
  : 'http://localhost:3000/payment/verify';

const response = await fetch('https://api.paystack.co/transaction/initialize', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'customer@example.com',
    amount: 500000,
    callback_url: callbackUrl,
  }),
});

When to Use Redirect Mode vs Popup Mode

Choose the right mode to avoid redirect issues entirely:

Use redirect mode when:

  • Your site is server-rendered (PHP, Django, Rails, server-side Node.js)
  • You want the checkout on a separate page (less frontend JavaScript needed)
  • You need to support browsers with strict popup blocking
  • Your payment flow involves multiple steps after payment (e.g., redirect to a different service)

Use popup mode when:

  • Your site is a single-page app (React, Vue, Angular, Next.js)
  • You want the checkout overlay on your page for a smoother experience
  • You want to handle the payment result in JavaScript without a page reload
  • Your users are on modern browsers that handle iframes well

You can also offer both: start with popup mode, and fall back to redirect mode if the popup fails to open. This covers edge cases where popup blockers or extensions interfere.

function initiatePayment(email, amount) {
  try {
    // Try popup mode first
    const handler = PaystackPop.setup({
      key: 'pk_test_xxxxx',
      email,
      amount,
      callback: (response) => {
        window.location.href = `/payment/verify?reference=${response.reference}`;
      },
    });
    handler.openIframe();
  } catch (error) {
    // Fallback: initialize via server and redirect
    fetch('/api/payments/initialize', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, amount }),
    })
      .then(res => res.json())
      .then(data => {
        window.location.href = data.authorization_url;
      });
  }
}

Verification: Confirm Redirects Work

Test the full redirect flow end to end:

Step 1: Test with a test card.

Use Paystack test card 4084 0840 8408 4081 with any future expiry date and any CVV. Complete the payment. Confirm you are redirected to your callback URL with the reference and trxref query parameters.

Step 2: Check the callback URL receives the right parameters.

// In your callback route, log what you receive
router.get('/payment/verify', (req, res) => {
  console.log('Query params:', req.query);
  // Should log: { trxref: 'abc123', reference: 'abc123' }
  // Both values are the same transaction reference

  // Continue with verification...
});

Step 3: Test the popup callback.

// Temporarily add logging to your popup callback
callback: (response) => {
  console.log('Popup callback fired with:', response);
  // Should log: { reference: 'abc123', trans: '...',  ... }
  // Then redirect
  window.location.href = `/payment/verify?reference=${response.reference}`;
},

Step 4: Test with a failed payment.

Use test card 4084 0840 8408 4081 with CVV 400 to simulate a declined card. Confirm your callback page handles the failed status gracefully instead of showing a success message.

Step 5: Test the onClose handler.

Open the popup and close it without paying. Confirm your onClose handler fires and shows an appropriate message to the customer.

Key Takeaways

  • ✓Paystack has two checkout modes: redirect and popup. Redirect mode uses callback_url to send the customer back. Popup mode uses a JavaScript callback function. Mixing them up is the most common cause of redirect failures.
  • ✓If you use /transaction/initialize without setting callback_url in the request body, Paystack falls back to the callback URL in your dashboard settings. If neither is set, the customer stays on the Paystack checkout page after payment.
  • ✓The callback_url in the API request always overrides the one in your dashboard. If you set a callback_url in both places and they differ, the API request wins.
  • ✓In popup mode (Inline.js / PaystackPop), there is no automatic redirect. The popup closes and your callback function runs. You must call window.location.href yourself to redirect the customer.
  • ✓Paystack appends ?trxref=REFERENCE&reference=REFERENCE to your callback_url. Your callback page must read these parameters and verify the transaction on your server before granting access.
  • ✓Browser popup blockers can prevent the Paystack popup from opening in the first place, which means the callback function never fires. Always trigger the popup from a direct user click.
  • ✓Never trust the redirect alone to confirm payment. A customer could manually navigate to your callback URL without paying. Always verify the transaction server-side using /transaction/verify.

Frequently Asked Questions

What is the difference between callback_url and webhook_url in Paystack?
callback_url is the page your customer is redirected to after payment (a browser redirect). webhook_url is the endpoint Paystack sends a server-to-server POST notification to (no browser involved). Both fire after payment, but they serve different purposes. Use callback_url for the user experience (showing a receipt) and webhooks for the backend logic (updating your database). Always rely on webhooks for critical business logic because a customer might close their browser before the redirect happens.
Why does Paystack add ?trxref and ?reference to my callback URL?
Paystack appends the transaction reference as both trxref and reference query parameters so your callback page can identify which transaction to verify. Both parameters contain the same value. Use either one to call /transaction/verify on your server.
My callback URL works in test mode but not in live mode. Why?
In live mode, Paystack requires your callback_url to use HTTPS. If your callback URL starts with http://, Paystack may not redirect to it in live mode. Make sure your production site uses HTTPS and your callback_url starts with https://. Also verify that your live API keys are active and your Paystack account is fully activated.
Can I change the callback URL after initializing a transaction?
No. The callback_url is set at transaction initialization time and cannot be changed afterward. If you need to redirect to a different page, set the correct callback_url before the customer starts the checkout. If you forgot to set it, you will need to initialize a new transaction with the correct URL.
The popup closes but the callback function does not fire. What happened?
If the customer closes the popup manually (clicks the X or presses Escape), the onClose function fires, not the callback function. The callback only fires when payment is successful. If the popup closes by itself after a successful payment and the callback does not fire, check for JavaScript errors in your console. A syntax error in your callback function will prevent it from running.

Learn Paystack Integration

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

Not ready? Create Free Account

Keep reading

Paystack Errors and Troubleshooting: The Complete Reference

Every Paystack error you will hit in production, what causes it, and how to fix it. Authentication failures, webhook signature problems, transaction errors, transfer issues, and frontend bugs. Code-first solutions for each one.

Read more

Paystack Popup Not Opening: Frontend Debug Guide

The Paystack Inline JS popup silently fails to open for several reasons: the script did not load, the browser blocked it as a popup, PaystackPop is undefined, the public key is wrong, or a Content Security Policy blocks the iframe. This guide walks through each cause with a verification step.

Read more

Paystack Checkout Blocked by Content Security Policy

Your Content Security Policy is blocking Paystack from loading its checkout script or iframe. You need to whitelist js.paystack.co, standard.paystack.co, and api.paystack.co in the right CSP directives.

Read more

Paystack Test Mode Working but Live Mode Failing

Your Paystack integration works perfectly in test mode but fails in live mode. The most common cause is an unactivated business account. Walk through this checklist to find and fix the exact issue.

Read more

Explore related topics

M-Pesa & African Stack

M-Pesa Daraja API tutorials, USSD development, and WhatsApp Business API guides. The African Stack that global bootcamps do not teach.

Learn to Code in Kenya

Everything a Kenyan needs to know before writing their first line of code. Career paths, real costs, hardware requirements, and honest answers about what it takes.

Learning to Code

Roadmaps, language guides, and practical advice for learning to code and launching a programming career.

Breaking Into Tech

Practical, no-nonsense guides for anyone looking to start or change their career in tech. From choosing your first language to acing interviews.

The African Stack: Skills for This Market

The skills that make Kenyan developers hard to replace: M-Pesa, USSD, WhatsApp, fintech, and building for the African market.

Bonaventure Ogeto

Bonaventure Ogeto

Founder, McTaba Labs

Bonaventure is the founder of McTaba Labs, a coding bootcamp in Nairobi, Kenya. He has spent years building products for the African market and mentoring the next generation of developers across the continent.