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. Accept Payments with Paystack in React
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Accept Payments with Paystack in React

A React SPA cannot call the Paystack API directly because it requires a secret key. Create a backend API (Express, Fastify, or any server) that initializes transactions and verifies payments. Your React frontend calls your backend, gets the authorization URL or access code, and either redirects to Paystack or opens the inline popup.

In this article

  1. 1. How React and Paystack Fit Together
  2. 2. Set Up the Backend API
  3. 3. Inline Popup Checkout in React
  4. 4. Redirect Checkout
  5. 5. Create a Custom usePaystack Hook
  6. 6. Security and Production Notes
  7. 7. Ship Payments Faster

How React and Paystack Fit Together

React is a frontend library. It runs in the user's browser. Paystack's API requires a secret key for initializing and verifying transactions. You cannot put a secret key in browser code because anyone can view it using browser dev tools.

The architecture looks like this:

  1. Your React frontend collects the customer's email and displays the amount.
  2. React calls your backend API with the email and amount.
  3. Your backend calls Paystack's Initialize Transaction endpoint with your secret key and returns the access code or authorization URL to React.
  4. React either redirects to the authorization URL or opens the Paystack inline popup with the access code.
  5. After payment, React calls your backend to verify the transaction.

This guide uses Express.js for the backend examples, but any backend works: Fastify, Koa, Django, FastAPI, Rails, Go, or anything that can make HTTP requests and serve JSON.

Set Up the Backend API

Create a minimal Express server with two endpoints: one to initialize and one to verify.

// server.js
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({ origin: 'http://localhost:3000' }));
app.use(express.json());

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;

// Initialize a transaction
app.post('/api/pay', async (req, res) => {
  const { email, amount } = req.body;
  const reference = 'order_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);

  const response = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + PAYSTACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email,
      amount: Math.round(amount * 100),
      reference,
    }),
  });

  const data = await response.json();

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

  // TODO: Save reference and expected amount to your database

  res.json({
    authorization_url: data.data.authorization_url,
    access_code: data.data.access_code,
    reference: data.data.reference,
  });
});

// Verify a transaction
app.get('/api/verify', async (req, res) => {
  const { reference } = req.query;

  const response = await fetch(
    'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
    {
      headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET },
    }
  );

  const data = await response.json();

  if (data.status && data.data.status === 'success') {
    // TODO: Check amount and currency against your database
    // TODO: Mark order as paid (idempotently)

    return res.json({
      success: true,
      amount: data.data.amount / 100,
      currency: data.data.currency,
    });
  }

  res.status(400).json({ success: false });
});

app.listen(4000, () => console.log('Server running on port 4000'));

Install the dependencies:

npm install express cors

Run the server with your secret key:

PAYSTACK_SECRET_KEY=sk_test_xxx node server.js

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Inline Popup Checkout in React

The inline popup is the most popular approach for React apps. The customer stays on your page, and Paystack's checkout opens as an overlay.

First, load the Paystack inline script. You can add it to your index.html or load it dynamically in a component:

// src/PaystackCheckout.jsx
import { useState, useEffect } from 'react';

function PaystackCheckout() {
  const [email, setEmail] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://js.paystack.co/v2/inline.js';
    script.async = true;
    document.head.appendChild(script);
    return () => { document.head.removeChild(script); };
  }, []);

  async function handlePay() {
    setLoading(true);

    try {
      const res = await fetch('http://localhost:4000/api/pay', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, amount: 5000 }),
      });

      const data = await res.json();

      if (!data.access_code) {
        alert(data.error || 'Initialization failed');
        setLoading(false);
        return;
      }

      const popup = new window.PaystackPop();
      popup.checkout({
        accessCode: data.access_code,
        onSuccess: async (transaction) => {
          // Verify on the server
          const verifyRes = await fetch(
            'http://localhost:4000/api/verify?reference=' + transaction.reference
          );
          const result = await verifyRes.json();

          if (result.success) {
            alert('Payment confirmed! ' + result.currency + ' ' + result.amount);
          } else {
            alert('Verification failed. Contact support.');
          }
          setLoading(false);
        },
        onCancel: () => {
          console.log('Payment cancelled');
          setLoading(false);
        },
      });
    } catch (err) {
      alert('Network error. Please try again.');
      setLoading(false);
    }
  }

  return (
    <div style={{ maxWidth: 400, margin: '80px auto' }}>
      <h1>Checkout</h1>
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        style={{ width: '100%', padding: 8, marginBottom: 16 }}
      />
      <p>Amount: NGN 5,000</p>
      <button onClick={handlePay} disabled={loading || !email}>
        {loading ? 'Processing...' : 'Pay Now'}
      </button>
    </div>
  );
}

export default PaystackCheckout;

The flow: user clicks "Pay Now," your React app calls your Express backend, your backend calls Paystack and returns an access code, React opens the Paystack popup with that access code. After the customer pays, the onSuccess callback fires, and you verify on your backend before showing a confirmation.

Redirect Checkout

If you prefer the full-page redirect approach (simpler, but the customer leaves your React app), use the authorization URL instead of the access code:

async function handleRedirectPay() {
  const res = await fetch('http://localhost:4000/api/pay', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, amount: 5000 }),
  });

  const data = await res.json();

  if (data.authorization_url) {
    window.location.href = data.authorization_url;
  } else {
    alert(data.error || 'Failed to initialize payment');
  }
}

After payment, Paystack redirects to your callback URL with ?reference=REF. If your React app uses React Router, create a route for the callback path and verify the reference on mount:

// src/PaymentCallback.jsx
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';

function PaymentCallback() {
  const [searchParams] = useSearchParams();
  const [status, setStatus] = useState('verifying');
  const reference = searchParams.get('reference') || searchParams.get('trxref');

  useEffect(() => {
    if (!reference) {
      setStatus('error');
      return;
    }

    fetch('http://localhost:4000/api/verify?reference=' + reference)
      .then((res) => res.json())
      .then((data) => {
        setStatus(data.success ? 'success' : 'failed');
      })
      .catch(() => setStatus('error'));
  }, [reference]);

  if (status === 'verifying') return <p>Verifying payment...</p>;
  if (status === 'success') return <h1>Payment Successful!</h1>;
  return <h1>Payment Failed. Please try again.</h1>;
}

export default PaymentCallback;

Set the callback URL in your backend initialization to point to this route (for example, http://localhost:3000/payment/callback).

Create a Custom usePaystack Hook

If you accept payments on multiple pages, extract the logic into a reusable hook:

// src/hooks/usePaystack.js
import { useEffect, useRef } from 'react';

export function usePaystack() {
  const scriptLoaded = useRef(false);

  useEffect(() => {
    if (scriptLoaded.current) return;
    const script = document.createElement('script');
    script.src = 'https://js.paystack.co/v2/inline.js';
    script.async = true;
    script.onload = () => { scriptLoaded.current = true; };
    document.head.appendChild(script);
  }, []);

  async function pay({ email, amount, onSuccess, onCancel }) {
    const res = await fetch('http://localhost:4000/api/pay', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, amount }),
    });

    const data = await res.json();

    if (!data.access_code) {
      throw new Error(data.error || 'Initialization failed');
    }

    const popup = new window.PaystackPop();
    popup.checkout({
      accessCode: data.access_code,
      onSuccess: async (transaction) => {
        const verifyRes = await fetch(
          'http://localhost:4000/api/verify?reference=' + transaction.reference
        );
        const result = await verifyRes.json();
        if (result.success) {
          onSuccess(result);
        } else {
          throw new Error('Verification failed');
        }
      },
      onCancel: onCancel || (() => {}),
    });

    return data.reference;
  }

  return { pay };
}

Usage in any component:

import { usePaystack } from './hooks/usePaystack';

function BuyButton({ email, amount, productName }) {
  const { pay } = usePaystack();

  async function handleClick() {
    try {
      await pay({
        email,
        amount,
        onSuccess: (result) => {
          alert('Paid ' + result.currency + ' ' + result.amount + ' for ' + productName);
        },
      });
    } catch (err) {
      alert(err.message);
    }
  }

  return <button onClick={handleClick}>Buy {productName}</button>;
}

Security and Production Notes

Before going live, verify these points:

  • CORS. In production, restrict your backend CORS policy to your React app's domain. Do not use cors({ origin: '*' }).
  • Backend URL. Replace http://localhost:4000 with your production backend URL. Use an environment variable in your React app.
  • HTTPS. Both your React app and your backend must use HTTPS in production. Paystack requires HTTPS for callback URLs.
  • Amount validation. Validate the amount on the backend. Do not trust the amount sent from the frontend. Look up the correct amount from your database based on the product or order ID.
  • Webhooks. Set up a webhook endpoint on your backend. The popup callback and redirect callback can both fail if the customer closes their browser. Webhooks are the only reliable delivery mechanism.
  • Live keys. Switch from sk_test_ to sk_live_ in your production environment. Never hardcode keys in your source code.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series. We cover webhooks, verification, subscriptions, and more for every major stack.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓React runs in the browser. You cannot use your Paystack secret key in React code. All Paystack API calls must go through your backend.
  • ✓The inline popup (PaystackPop) is the most common pattern for React apps. It keeps the user on your page while Paystack handles the checkout UI.
  • ✓After the popup closes with a success callback, verify the transaction on your backend before granting value. The popup callback is not proof of payment.
  • ✓Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Convert before sending to your backend or let the backend handle the conversion.
  • ✓Use a loading state on the pay button to prevent double-clicks. Combine with a deterministic transaction reference to prevent duplicate charges.
  • ✓Set up webhooks on your backend. They fire even when the popup is closed or the browser crashes mid-payment.

Frequently Asked Questions

Do I need react-paystack or paystack-inline-js npm packages?
No. Community packages like react-paystack wrap the Paystack inline script, but they add a dependency you do not control. Loading the official Paystack inline script from the CDN and calling it directly gives you the same functionality with less abstraction. If you prefer a package for convenience, that is fine, but it is not required.
Can I initialize a Paystack transaction directly from React without a backend?
No. The Initialize Transaction endpoint requires your secret key in the Authorization header. If you put that key in React code, anyone who opens browser dev tools can see it and make charges against your account. You must use a backend.
How do I handle the popup closing unexpectedly?
The onCancel callback fires when the customer closes the popup before completing payment. If the popup is closed after the customer authorized the charge (entered PIN/OTP), the payment may still complete on Paystack side. Your webhook handler will catch these cases and fulfill the order. Never rely solely on the popup callbacks.
What backend should I use with React for Paystack?
Any backend that can make HTTP requests and serve JSON works. Express.js and Fastify are popular choices for JavaScript developers. If your team uses Python, Django or FastAPI work great. For PHP shops, Laravel is common. The Paystack API is language-agnostic. Pick whatever your team already knows.
Can I test Paystack in React without real money?
Yes. Use test keys (sk_test_ and pk_test_). Paystack provides test card numbers for simulating successful and failed transactions. No real money moves when using test keys. Switch to live keys only when you deploy to production.

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 Integration Guides by Language and Framework

A complete directory of Paystack integration guides organized by programming language and framework. Each guide covers accepting payments, verifying transactions, handling webhooks, and building subscriptions in your specific stack.

Read more

Verify Paystack Payments in Svelte and SvelteKit

Verify Paystack transactions in SvelteKit using server endpoints for popup verification and load functions for redirect callback verification. Covers amount validation, idempotent fulfillment, and SSR verification for instant results.

Read more

Handle Paystack Webhooks in Flask

Receive and verify Paystack webhooks in your Flask application. This guide covers accessing request.data for raw bytes, HMAC SHA-512 signature verification, processing charge.success events, and making handlers idempotent.

Read more

Build Paystack Subscriptions in ASP.NET Core

Implement Paystack recurring billing in ASP.NET Core. This guide covers creating plans, subscribing customers through transaction initialization, handling renewal webhooks with C#, and cancelling subscriptions.

Read more

Building a Wallet Top-Up Flow on Paystack

A step-by-step guide to building wallet top-up flows with Paystack. Initialize payment, handle webhooks, credit wallets safely, enforce minimum amounts, and prevent double-crediting in production.

Read more

Paystack Rate Limit and 429 Handling

Paystack returns 429 Too Many Requests when you exceed their API rate limit. This guide covers the actual limits, how to implement exponential backoff, when to cache responses, and how to batch operations to avoid hitting the limit.

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.