Bonaventure OgetoBy Bonaventure Ogeto|

Why You Must Never Call the Paystack API From Your Frontend

Never call the Paystack API from your frontend because it requires your secret key in the Authorization header. Any code that runs in the browser is visible to the user. If your secret key is in frontend JavaScript, anyone can extract it from the browser DevTools and use it to initiate refunds, view customer data, or modify your account. The correct pattern is: your frontend sends a request to your own backend server, and your backend server calls Paystack with the secret key.

The Mistake That Every Beginner Makes

Here is what the wrong approach looks like. You are building a React or vanilla JavaScript app, you need to initialize a Paystack transaction, and you write something like this:

// WRONG: Do NOT do this. Secret key is in frontend code.
async function initializePayment(email, amount) {
  var response = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer sk_live_abc123def456', // YOUR SECRET KEY IN THE BROWSER
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: amount * 100,
    }),
  });

  var data = await response.json();
  window.location.href = data.data.authorization_url;
}

This code will not even work (CORS will block it), but even if it did, it would be a security disaster. Let us break down exactly what goes wrong.

What Happens When Your Secret Key Is Exposed

Your Paystack secret key is the master key to your payment account. With it, someone can:

  • Initiate refunds on any transaction, draining your Paystack balance
  • View all customer data, including emails, names, and payment history
  • Create transfers to send money from your Paystack balance to any bank account
  • List all transactions, seeing every payment your business has received
  • Charge saved authorization codes, billing your existing customers without their consent
  • Modify your account settings, including webhook URLs (redirecting your webhook notifications to their server)

Any JavaScript that runs in the browser is fully visible to the user. There is no way to hide it.

"But I minified and bundled my code"

Minification renames variables and removes whitespace. It does not encrypt or hide string literals. Your secret key is still sitting in the bundle as a plain string. Open the browser DevTools, go to Sources, and search for "sk_live_". It will be right there.

"But I used an environment variable in my React app"

If you are using Create React App, Next.js (client-side), or Vite, and you reference process.env.REACT_APP_PAYSTACK_SECRET or import.meta.env.VITE_PAYSTACK_SECRET in a component, the build tool replaces the variable with the actual value at build time. The compiled JavaScript file contains the literal secret key string. Environment variables in frontend frameworks are compile-time substitutions, not runtime secrets.

"But I put it in a .env file"

The .env file is not shipped to the browser. But if your frontend build process reads the variable and injects it into the bundle, the value ends up in the browser anyway. A .env file protects your key from being committed to Git. It does not protect it from being shipped to the browser if your frontend code references it.

CORS Will Block You Anyway

Even if you did not care about security (you should), the browser will not let you call the Paystack API directly. You will get an error like this in your console:

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

CORS (Cross-Origin Resource Sharing) is a browser security feature. When JavaScript on https://yoursite.com tries to make a request to https://api.paystack.co, the browser first sends a preflight OPTIONS request to Paystack asking if yoursite.com is allowed to make requests. Paystack's API does not include your domain in its Access-Control-Allow-Origin response, so the browser blocks the request.

This is intentional. Paystack's API is designed to be called from servers, not from browsers. The CORS restriction is a safety net that prevents accidental frontend usage. But do not rely on CORS as your security mechanism. CORS only protects against browser-based JavaScript. A malicious user with your secret key can call the API from Postman, curl, or their own server without any CORS restrictions.

Some developers try to work around CORS by using a proxy. They set up a server-side proxy that forwards requests from the frontend to Paystack. If you are doing this, you are already halfway to the correct solution. Just move the Paystack API call logic to the proxy server and keep the secret key there.

The Correct Pattern: Frontend to Server to Paystack

The right architecture has three parts. Your frontend talks to your server. Your server talks to Paystack. The secret key never leaves your server.

Customer's Browser  --->  Your Server  --->  Paystack API
    (public key only)       (secret key)        (processes payment)

Step 1: Frontend sends payment details to your server

// Frontend code (React, vanilla JS, etc.)
async function handlePayClick() {
  var response = await fetch('/api/initialize-payment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: customerEmail,
      amount: 5000, // Human-readable amount, your server converts to kobo
      orderId: currentOrderId,
    }),
  });

  var data = await response.json();

  // Option A: Redirect to Paystack checkout
  window.location.href = data.authorization_url;

  // Option B: Open popup with Paystack Inline JS
  // var popup = new PaystackPop();
  // popup.checkout({ accessCode: data.access_code, ... });
}

Step 2: Your server calls Paystack with the secret key

// Server code (Node.js + Express)
var express = require('express');
var app = express();
app.use(express.json());

app.post('/api/initialize-payment', async function(req, res) {
  var email = req.body.email;
  var amount = req.body.amount;
  var orderId = req.body.orderId;

  // Validate the request
  if (!email || !amount || !orderId) {
    return res.status(400).json({ error: 'Missing required fields' });
  }

  // Look up the order to confirm the amount matches
  var order = await db.orders.findById(orderId);
  if (!order || order.amount !== amount) {
    return res.status(400).json({ error: 'Invalid order' });
  }

  // Call Paystack from the server with the secret key
  var 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: email,
      amount: Math.round(amount * 100), // Convert to kobo
      reference: 'order_' + orderId,
      callback_url: 'https://yoursite.com/payment/callback',
    }),
  });

  var data = await response.json();

  if (data.status) {
    // Send the authorization URL and access code to the frontend
    // The secret key stays on the server
    res.json({
      authorization_url: data.data.authorization_url,
      access_code: data.data.access_code,
      reference: data.data.reference,
    });
  } else {
    res.status(400).json({ error: data.message });
  }
});

Step 3: Frontend uses the authorization URL or access code

Your frontend receives the authorization URL or access code from your server and either redirects the customer or opens a Paystack popup. The secret key never appeared in any frontend code. The customer's browser never made a request to api.paystack.co.

Why Server-Side Validation Matters Too

Moving the API call to the server is not just about hiding the key. It also lets you validate the payment request before it reaches Paystack.

If the frontend calls Paystack directly, a malicious user can modify the request. They can change the amount to 1 kobo, change the email, or remove the reference. Your server has no chance to validate the request because it never sees it.

With the correct pattern, your server sits between the frontend and Paystack. Before calling Paystack, your server can:

  • Verify the amount: Look up the order in your database and confirm the amount matches. This prevents a user from modifying the amount in their browser to pay less.
  • Generate the reference: Create a deterministic reference server-side instead of trusting a reference from the frontend. This prevents duplicate charges and reference manipulation.
  • Check authorization: Make sure the logged-in user is allowed to make this payment. Check that the order belongs to them.
  • Rate limit: Prevent a malicious script from spamming your payment endpoint and creating thousands of pending transactions on your Paystack account.
  • Log the attempt: Record who initiated the payment, when, and for what. This is invaluable for debugging and auditing.
// Server-side validation before calling Paystack
app.post('/api/initialize-payment', async function(req, res) {
  var userId = req.session.userId; // From your auth middleware
  var orderId = req.body.orderId;

  // 1. Check user is logged in
  if (!userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }

  // 2. Look up the order
  var order = await db.orders.findById(orderId);
  if (!order) {
    return res.status(404).json({ error: 'Order not found' });
  }

  // 3. Check the order belongs to this user
  if (order.userId !== userId) {
    return res.status(403).json({ error: 'Not your order' });
  }

  // 4. Check the order is not already paid
  if (order.status === 'paid') {
    return res.status(400).json({ error: 'Order already paid' });
  }

  // 5. Use the order amount from the database, NOT from the request
  var amountInKobo = Math.round(order.amount * 100);

  // 6. Generate a deterministic reference
  var reference = 'order_' + orderId;

  // Now call Paystack with validated, trusted data
  // ...
});

Notice that the amount comes from the database, not from the request body. This is critical. If you take the amount from the frontend, a user can change it to any value they want.

Public Key vs Secret Key: What Goes Where

Paystack gives you two key pairs (one for test, one for live):

  • Public key (pk_test_ or pk_live_): Safe to use in frontend code. Used by Paystack Inline JS to identify your account. Cannot be used to call the Paystack API. Cannot be used to read data, initiate refunds, or make transfers.
  • Secret key (sk_test_ or sk_live_): Must stay on your server. Used in the Authorization header for all API calls. Has full access to your Paystack account.

Where each key belongs:

Key Where How
Public key (pk_) Frontend code Passed to PaystackPop configuration or Inline JS
Secret key (sk_) Server environment variables process.env.PAYSTACK_SECRET_KEY (never in source code)

The public key appears in your HTML when you use Paystack Inline JS:

<script src="https://js.paystack.co/v2/inline.js"></script>
<script>
  var popup = new PaystackPop();
  popup.checkout({
    key: 'pk_live_your_public_key_here', // Public key is fine here
    accessCode: accessCodeFromYourServer,
    onSuccess: function(transaction) {
      // Verify on your server
    },
  });
</script>

This is safe. The public key only identifies your Paystack account to the Inline JS library. It cannot be used to call the REST API, view transactions, or move money. It is designed to be public.

What to Do If Your Key Is Already Exposed

If you have already shipped frontend code containing your secret key, act immediately:

  1. Log into your Paystack dashboard and go to Settings, then API Keys & Webhooks.
  2. Generate a new secret key. This invalidates the old one instantly.
  3. Update your server with the new secret key in your environment variables.
  4. Check your transaction history for any unauthorized activity. Look for refunds, transfers, or charges you did not initiate.
  5. Remove the old key from your frontend code and redeploy.
  6. Check your Git history. If the key was committed to a repository, it is still in the Git history even after you remove it from the current code. If the repository is public (or was public at any point), assume the key is compromised.

Key rotation is instant on Paystack's end. The moment you generate a new key, the old one stops working. Any requests using the old key will fail with a 401 Unauthorized error. Make sure you update all servers and services that use the key before or immediately after rotation.

If you find unauthorized transactions, contact Paystack support immediately. They can help investigate and potentially reverse fraudulent activity.

Patterns for Common Frameworks

Here is how the correct pattern looks in different setups you are likely using.

Next.js (API Route)

// pages/api/initialize-payment.js (or app/api/initialize-payment/route.js)
// This runs on the server, never in the browser

module.exports = async function handler(req, res) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  var 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: req.body.email,
      amount: Math.round(req.body.amount * 100),
    }),
  });

  var data = await response.json();
  res.json(data.data);
};

Express.js (standalone backend)

The examples earlier in this article use Express. The key point: your Express server is a separate process from your frontend. The frontend calls /api/initialize-payment on your Express server, and Express calls Paystack.

Serverless functions (Vercel, Netlify, AWS Lambda)

Serverless functions run on the server. They are a valid place for your secret key. Set PAYSTACK_SECRET_KEY as an environment variable in your serverless platform's dashboard (Vercel Environment Variables, Netlify Environment Variables, AWS Lambda environment configuration).

The universal rule applies to all frameworks: if the code runs in the browser, no secret key. If the code runs on a server (including serverless functions, API routes, and Edge Functions), the secret key is safe there.

Stay Up to Date

Security best practices evolve as platforms update their features. We keep these guides current when Paystack changes their key management or security recommendations.

Join the McTaba newsletter to get notified about new Paystack integration guides and security updates.

Sign up for the McTaba newsletter

Key Takeaways

  • Your Paystack secret key (sk_live_ or sk_test_) must never appear in frontend code. It belongs on your server, in environment variables. Anyone who has your secret key can refund transactions, view customer data, and transfer money from your account.
  • Even if you "hide" the key in a bundled JavaScript file, it is not hidden. Browser DevTools, source maps, and simple string searches in the bundle file will expose it.
  • CORS will block direct frontend-to-Paystack API calls anyway. Paystack does not set Access-Control-Allow-Origin headers for browser requests to their API endpoints. You will get a CORS error before the request even reaches Paystack.
  • The correct pattern has three parts: (1) frontend sends payment details to your server, (2) your server calls Paystack API with the secret key, (3) your server returns the authorization URL or access code to the frontend.
  • Paystack Inline JS (the popup library) is the only Paystack code that runs in the browser. It uses your public key (pk_live_ or pk_test_), not your secret key. Public keys are safe to expose.
  • If you have already exposed a secret key in frontend code, rotate it immediately in your Paystack dashboard. Generate a new key and update your server configuration.

Frequently Asked Questions

Can I use my Paystack public key to call the API?
No. The Paystack REST API requires your secret key in the Authorization header. Public keys are only used with Paystack Inline JS (the popup checkout library) to identify your account. Public keys cannot initialize transactions, verify payments, or access any API endpoint.
Is it safe to put my Paystack public key in React/Vue/Angular code?
Yes. Public keys (pk_live_ or pk_test_) are designed to be used in frontend code. They identify your Paystack account to the Inline JS library but cannot be used to call the API, view transactions, or move money. Paystack designed the key pair system specifically so that public keys are safe for client-side use.
My Paystack call works fine from Postman but fails from the browser with a CORS error. Why?
Postman is not a browser and does not enforce CORS restrictions. Browsers block cross-origin requests to APIs that do not explicitly allow them via CORS headers. Paystack does not set Access-Control-Allow-Origin for browser requests to their API. This is by design. Move your API call to a server-side endpoint.
Can I use a CORS proxy to call Paystack from the frontend?
Technically you could route requests through a CORS proxy, but this is worse than calling Paystack directly. Now your secret key passes through a third-party proxy server that can log it. If you are setting up a proxy anyway, put the Paystack API call on your proxy server and keep the secret key there. That is the correct pattern.
I committed my Paystack secret key to a public GitHub repository. What should I do?
Rotate your key immediately in the Paystack dashboard. Generate a new secret key, update your server, and assume the old key is compromised. Even if you remove the key from the repository, it remains in Git history. Check your Paystack dashboard for unauthorized transactions. GitHub has secret scanning that may have already flagged the key, but do not wait for that notification.

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