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 CORS Errors and What They Really Mean
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Paystack CORS Errors and What They Really Mean

Paystack CORS errors happen because you are calling the Paystack API (api.paystack.co) directly from browser JavaScript. Paystack intentionally does not set CORS headers on its API because server-to-server calls do not need them, and allowing browser calls would expose your secret key. The fix is to move the API call to your backend server. Create a server endpoint that your frontend calls, and have that endpoint call the Paystack API with your secret key. Never call api.paystack.co from frontend code.

In this article

  1. 1. What the CORS Error Looks Like
  2. 2. What CORS Actually Means (And Why Adding Headers Will Not Help)
  3. 3. The Correct Architecture: Frontend to Server to Paystack
  4. 4. The Popup Pattern (No Server Call Needed for Initialization)
  5. 5. Correct Pattern in Next.js
  6. 6. Mistakes That Lead to CORS Errors
  7. 7. Why This Matters: The Security Problem
  8. 8. Verification: Confirm the CORS Error Is Gone

What the CORS Error Looks Like

When you call the Paystack API from frontend JavaScript, the browser blocks the request and shows an error like this:

Access to fetch at 'https://api.paystack.co/transaction/initialize'
from origin 'http://localhost:3000' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Or in some browsers:

Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at https://api.paystack.co/transaction/initialize.
(Reason: CORS header 'Access-Control-Allow-Origin' missing).

The code that triggers this typically looks like:

// This code is running in the BROWSER. That is the problem.
const response = await fetch('https://api.paystack.co/transaction/initialize', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sk_test_xxxxx',  // Secret key exposed in browser!
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ email: 'user@example.com', amount: 50000 }),
});
// Result: CORS error. The request never reaches Paystack.

This code has two problems: the CORS error, and the fact that your secret key is exposed to anyone who opens browser DevTools.

What CORS Actually Means (And Why Adding Headers Will Not Help)

CORS (Cross-Origin Resource Sharing) is a browser security feature. When your frontend JavaScript on localhost:3000 tries to call api.paystack.co, the browser checks whether Paystack's server allows requests from your domain. It does this by looking for an Access-Control-Allow-Origin header in Paystack's response.

Paystack does not include this header. On purpose.

Here is the part most developers get wrong: you cannot fix this by adding CORS headers to your own server. The CORS check happens on the target server (Paystack's server), not on your server. You do not control Paystack's response headers.

Three "fixes" that do not work:

  • Adding CORS middleware to your Express/Next.js server. This adds headers to your server's responses, not to Paystack's responses. Your browser is complaining about Paystack's headers, not yours.
  • Using a CORS proxy. Services like cors-anywhere route your request through a proxy that strips CORS restrictions. For a payment API, this means your secret key passes through a third-party server. Never do this with financial data.
  • Browser extensions that disable CORS. These only work on your machine. Your users do not have the extension. And they disable a security feature that exists for good reason.

The real fix is architectural: move the API call to your server.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

The Correct Architecture: Frontend to Server to Paystack

The correct Paystack integration has three steps:

  1. Your frontend sends payment details to your server
  2. Your server calls Paystack's API with your secret key
  3. Your server returns the result to your frontend

The browser never talks to api.paystack.co directly. Only your server does. Server-to-server calls do not have CORS restrictions because CORS is a browser-only security feature.

// Step 1: Frontend calls YOUR server
// frontend/checkout.js

async function initiatePayment(email, amount) {
  const response = await fetch('/api/payments/initialize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, amount }),
  });

  const data = await response.json();

  if (data.authorization_url) {
    // Redirect to Paystack checkout
    window.location.href = data.authorization_url;
  }
}
// Step 2: Your server calls Paystack
// server/routes/payments.js

const express = require('express');
const router = express.Router();

router.post('/api/payments/initialize', async (req, res) => {
  const { email, amount } = req.body;

  // Server-to-server call. No CORS. Secret key stays on the server.
  const paystackResponse = 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,
      amount: amount * 100,  // Convert to kobo
    }),
  });

  const data = await paystackResponse.json();

  // Step 3: Return the result to the frontend
  res.json(data.data);
});

module.exports = router;

Notice that the secret key (PAYSTACK_SECRET_KEY) only exists on the server. The frontend never sees it. The frontend only knows the URL of your own server endpoint.

The Popup Pattern (No Server Call Needed for Initialization)

If you use Paystack's Inline.js popup, you do not need to call /transaction/initialize at all. The popup handles initialization internally using your public key:

<script src="https://js.paystack.co/v2/inline.js"></script>
<script>
  function payWithPaystack() {
    const handler = PaystackPop.setup({
      key: 'pk_test_xxxxx',  // Public key is safe in the browser
      email: 'customer@example.com',
      amount: 50000,  // 500 NGN in kobo
      currency: 'NGN',
      callback: function(response) {
        // Payment complete. Verify on your server.
        fetch('/api/payments/verify', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ reference: response.reference }),
        });
      },
      onClose: function() {
        console.log('Payment window closed');
      },
    });
    handler.openIframe();
  }
</script>

With this pattern, the only server call you need is verification after the payment completes. The popup uses the public key (pk_test_ or pk_live_), which is safe to expose in the browser.

You still need a server endpoint for verification:

// server/routes/payments.js
router.post('/api/payments/verify', async (req, res) => {
  const { reference } = req.body;

  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') {
    // Grant access, send receipt, update database
    res.json({ verified: true });
  } else {
    res.json({ verified: false, status: data.data.status });
  }
});

The verification call uses your secret key, so it must happen on the server. If you try to call /transaction/verify from the browser, you will get the same CORS error.

Correct Pattern in Next.js

In Next.js, your API route acts as the server layer. The frontend component calls the API route, and the API route calls Paystack:

// app/api/payments/initialize/route.ts (App Router)
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { email, amount } = await request.json();

  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, amount: amount * 100 }),
  });

  const data = await response.json();

  if (!data.status) {
    return NextResponse.json({ error: data.message }, { status: 400 });
  }

  return NextResponse.json({
    authorization_url: data.data.authorization_url,
    reference: data.data.reference,
  });
}
// app/checkout/page.tsx (Client Component)
'use client';

async function handlePayment() {
  const response = await fetch('/api/payments/initialize', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: 'customer@email.com',
      amount: 5000,
    }),
  });

  const data = await response.json();

  if (data.authorization_url) {
    window.location.href = data.authorization_url;
  }
}

The client component calls /api/payments/initialize on your own Next.js server. No CORS issues because the request stays on the same origin. The API route then calls Paystack server-to-server.

Mistakes That Lead to CORS Errors

These are the patterns that cause CORS errors. If you recognize any of them in your code, move the call to your server.

Mistake 1: Calling /transaction/initialize from the frontend.

// WRONG: This runs in the browser
const res = await axios.post('https://api.paystack.co/transaction/initialize', {
  email: 'user@example.com',
  amount: 50000,
}, {
  headers: { Authorization: 'Bearer sk_test_xxxxx' },
});
// CORS error + secret key exposed

Mistake 2: Calling /transaction/verify from the frontend.

// WRONG: Verification must happen on the server
const res = await fetch(`https://api.paystack.co/transaction/verify/${reference}`, {
  headers: { Authorization: 'Bearer sk_test_xxxxx' },
});
// CORS error + secret key exposed

Mistake 3: Calling any Paystack endpoint from a React useEffect or event handler.

// WRONG: useEffect runs in the browser
useEffect(() => {
  fetch('https://api.paystack.co/bank', {
    headers: { Authorization: 'Bearer sk_test_xxxxx' },
  }).then(res => res.json()).then(setBanks);
}, []);
// CORS error + secret key exposed

Mistake 4: Using a CORS proxy to bypass the restriction.

// DANGEROUS: Your secret key passes through a third party
const res = await fetch('https://cors-anywhere.herokuapp.com/https://api.paystack.co/transaction/initialize', {
  headers: { Authorization: 'Bearer sk_test_xxxxx' },
});
// "Works" but your secret key is now visible to the proxy operator

In every case, the fix is the same: create a server endpoint and move the Paystack call there.

Why This Matters: The Security Problem

The CORS error is actually saving you from a security breach. If Paystack allowed browser-origin API calls, your secret key would be visible to anyone who opens browser DevTools on your site. With your secret key, an attacker could:

  • Initialize transactions on your account
  • View all your customer data
  • Initiate transfers from your Paystack balance
  • List all your transaction history
  • Create and manage subaccounts

This is why Paystack gives you two key types:

  • Public keys (pk_test_, pk_live_) are safe for the browser. They can only initialize the checkout popup. They cannot read data or initiate transfers.
  • Secret keys (sk_test_, sk_live_) have full API access. They must stay on your server.

If your secret key has ever been exposed in frontend code (even in a Git commit that was later deleted), regenerate it immediately in your Paystack dashboard under Settings > API Keys & Webhooks.

Verification: Confirm the CORS Error Is Gone

After moving your Paystack API calls to the server, verify the fix:

Step 1: Check your frontend code.

Search your entire frontend codebase for api.paystack.co. There should be zero matches. Every Paystack API call should go through your own server endpoints.

# Search for direct Paystack API calls in frontend code
grep -r "api.paystack.co" src/
# Should return no results (or only results in server-side files)

Step 2: Check your frontend code for secret keys.

# Search for secret keys in frontend code
grep -r "sk_test_|sk_live_" src/
# Should return no results

Step 3: Test the payment flow.

Open your browser DevTools, go to the Console tab, and complete a test payment. There should be no CORS errors. The Network tab should show requests to your own server (e.g., /api/payments/initialize), not to api.paystack.co.

Step 4: Verify the server endpoint works.

// Quick test from your terminal (not the browser)
curl -X POST http://localhost:3000/api/payments/initialize   -H "Content-Type: application/json"   -d '{"email": "test@example.com", "amount": 500}'

// Should return { "authorization_url": "https://checkout.paystack.com/...", ... }

If the curl command works but the browser still shows CORS errors, then you do need CORS headers on your own server, but only for your own endpoints. This is different from the original problem. Use the cors middleware in Express or configure CORS in your framework to allow your frontend origin.

Key Takeaways

  • ✓CORS errors on Paystack mean you are calling the Paystack API from browser JavaScript. This is a security mistake, not a configuration problem.
  • ✓Paystack intentionally blocks browser-origin API calls. Adding CORS headers to your own server does not fix this because the error is on Paystack's domain, which you do not control.
  • ✓The correct pattern: your frontend calls your server, your server calls Paystack. Your secret key never touches the browser.
  • ✓The only Paystack resource designed for browsers is the Inline.js checkout script (js.paystack.co/v2/inline.js). Everything else goes through your server.
  • ✓If you see "No Access-Control-Allow-Origin header is present on the requested resource", that is Paystack telling you this call should come from a server.
  • ✓CORS proxies and browser extensions that bypass CORS are dangerous for payment integrations. They expose your secret key to anyone watching network traffic.
  • ✓Using fetch() or axios to call api.paystack.co from React, Vue, Angular, or vanilla JS will always fail. This is by design.

Frequently Asked Questions

Why does Paystack not support CORS?
Paystack intentionally blocks browser-origin API calls because they would require exposing your secret key in frontend code. Secret keys have full API access including transfers and customer data. By not setting CORS headers, Paystack forces the correct architecture: your frontend talks to your server, and your server talks to Paystack.
Can I add CORS headers to my server to fix Paystack CORS errors?
No. The CORS error is on Paystack's domain (api.paystack.co), not on yours. Adding CORS headers to your Express or Next.js server only affects responses from your server. You cannot control the headers that Paystack sends. The fix is to stop calling api.paystack.co from the browser entirely.
Is it safe to use a CORS proxy for Paystack API calls?
No. A CORS proxy routes your request through a third-party server, which means your secret key passes through that server in plain text. For a payment API, this is a serious security risk. The proxy operator could see your secret key and misuse it. Always call Paystack from your own server.
Can I call any Paystack endpoint from the browser?
The only Paystack resource designed for browsers is the Inline.js checkout script at js.paystack.co/v2/inline.js. This script uses your public key to open the checkout popup. All other Paystack API calls (initialize, verify, transfer, customer, etc.) must happen on your server using your secret key.
I see CORS errors on my own API endpoint after adding a Paystack call. Why?
If your frontend and backend run on different origins (e.g., frontend on localhost:3000, backend on localhost:5000), you need CORS headers on your own server to allow the frontend to call it. This is a separate issue from calling Paystack. Add the cors middleware to your Express server or configure CORS in your framework to allow your frontend origin.

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 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 Mixed Content and HTTPS Errors

Your site is served over HTTP but Paystack scripts load over HTTPS. Modern browsers block this mix. The fix is to serve your entire site over HTTPS.

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

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.