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 Node.js and Express
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Accept Payments with Paystack in Node.js and Express

To accept Paystack payments in Express, create a POST route that initializes a transaction with the Paystack API using your secret key. Return the authorization URL to your frontend, redirect the user to Paystack checkout, then verify the payment in a GET callback route before granting value.

In this article

  1. 1. Prerequisites
  2. 2. Project Setup
  3. 3. Initialize a Transaction
  4. 4. Redirect Checkout Flow
  5. 5. Handle the Redirect Callback
  6. 6. Inline Popup Checkout
  7. 7. Security Checklist and Going Live
  8. 8. Ship Payments Faster

Prerequisites

Before you start, make sure you have:

  • A Paystack account with test keys ready (Settings > API Keys & Webhooks)
  • Node.js 18 or later installed (for built-in fetch support)
  • Basic familiarity with Express.js route handlers

This guide uses plain JavaScript with CommonJS requires. The same patterns work in TypeScript or ESM imports.

Project Setup

Create a new project and install Express:

mkdir paystack-express && cd paystack-express
npm init -y
npm install express dotenv

Create a .env file:

PAYSTACK_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxx
BASE_URL=http://localhost:3000

Create your main server file:

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

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

const PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
const BASE_URL = process.env.BASE_URL;

app.listen(3000, function() {
  console.log('Server running on port 3000');
});

No Paystack SDK is needed. Node 18+ includes a global fetch function. If you are on an older version, install node-fetch.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Initialize a Transaction

Create a POST route that receives the customer email and amount, calls the Paystack Initialize Transaction endpoint, and returns the authorization URL.

// Initialize a Paystack transaction
app.post('/api/pay', async function(req, res) {
  var email = req.body.email;
  var amount = req.body.amount;

  var reference = 'order_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);

  var paystackRes = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: {
      Authorization: 'Bearer ' + PAYSTACK_SECRET,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      email: email,
      amount: Math.round(amount * 100),
      reference: reference,
      callback_url: BASE_URL + '/api/payment/callback',
      metadata: {
        custom_fields: [
          {
            display_name: 'Payment For',
            variable_name: 'payment_for',
            value: 'Order ' + reference,
          },
        ],
      },
    }),
  });

  var data = await paystackRes.json();

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

  // TODO: Save reference + amount + email to your database here

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

Key details:

  • Math.round(amount * 100) converts the display amount to kobo and avoids floating-point issues.
  • The callback_url tells Paystack where to redirect after payment. We build that route next.
  • Save the reference to your database before returning. Both your webhook handler and callback route need it.

Redirect Checkout Flow

The simplest checkout approach: your frontend calls the initialize endpoint, gets the authorization URL, and redirects the browser.

<!-- public/checkout.html -->
<form id="checkout-form">
  <input type="email" id="email" placeholder="Email" required />
  <p>Amount: NGN 5,000</p>
  <button type="submit">Pay Now</button>
</form>

<script>
document.getElementById('checkout-form').addEventListener('submit', function(e) {
  e.preventDefault();
  var email = document.getElementById('email').value;

  fetch('/api/pay', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: email, amount: 5000 }),
  })
  .then(function(res) { return res.json(); })
  .then(function(data) {
    if (data.authorization_url) {
      window.location.href = data.authorization_url;
    } else {
      alert(data.error || 'Something went wrong');
    }
  });
});
</script>

When the user clicks Pay Now, the browser redirects to Paystack's hosted checkout page. Paystack handles card entry, 3DS verification, bank transfers, and all other payment channels.

Handle the Redirect Callback

After payment, Paystack redirects back to your callback URL with ?trxref=REFERENCE&reference=REFERENCE as query parameters. Create a GET route to handle this:

// Handle Paystack redirect callback
app.get('/api/payment/callback', async function(req, res) {
  var reference = req.query.reference || req.query.trxref;

  if (!reference) {
    return res.status(400).send('Missing reference');
  }

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

  var data = await paystackRes.json();

  if (data.status && data.data.status === 'success') {
    var amountPaid = data.data.amount / 100;
    var currency = data.data.currency;

    // TODO: Look up the order by reference in your database
    // TODO: Confirm amount and currency match what you expected
    // TODO: Mark the order as paid (check if already paid to prevent double-granting)

    return res.send(
      'Payment successful! Reference: ' + reference +
      ' Amount: ' + currency + ' ' + amountPaid
    );
  }

  res.status(400).send('Payment not successful. Status: ' + (data.data ? data.data.status : 'unknown'));
});

Three checks are non-negotiable: the transaction status must be "success", the amount must match what you stored during initialization, and the currency must be correct. Skipping any of these opens you to underpayment or currency-swap exploits.

Inline Popup Checkout

If you want the customer to stay on your page, use Paystack's inline popup. Load the Paystack Inline.js script and open the popup with the access code from your initialization endpoint:

<script src="https://js.paystack.co/v2/inline.js"></script>
<script>
function payWithPopup(email, amount) {
  fetch('/api/pay', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: email, amount: amount }),
  })
  .then(function(res) { return res.json(); })
  .then(function(data) {
    if (!data.access_code) {
      alert(data.error || 'Initialization failed');
      return;
    }

    var popup = new PaystackPop();
    popup.checkout({
      accessCode: data.access_code,
      onSuccess: function(transaction) {
        // Verify on your server
        fetch('/api/verify?reference=' + transaction.reference)
        .then(function(res) { return res.json(); })
        .then(function(result) {
          if (result.success) {
            alert('Payment confirmed!');
          }
        });
      },
      onCancel: function() {
        console.log('Customer closed popup');
      },
    });
  });
}
</script>

Add a separate verify route for the popup flow:

app.get('/api/verify', async function(req, res) {
  var reference = req.query.reference;
  if (!reference) return res.status(400).json({ error: 'Missing reference' });

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

  var data = await paystackRes.json();

  if (data.status && data.data.status === 'success') {
    return res.json({ success: true, amount: data.data.amount / 100, currency: data.data.currency });
  }

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

Security Checklist and Going Live

  1. Secret key is server-only. Never send your secret key to the frontend. It stays in .env and is read by Express.
  2. Validate amounts server-side. Look up the correct price from your database. Never trust the amount the frontend sends without checking it.
  3. Verify every transaction. Never grant value based on the redirect alone. Always call the verify endpoint and check status, amount, and currency.
  4. Set up webhooks. Webhooks are the reliable delivery mechanism. The redirect can fail if the customer closes their browser. See Handle Paystack Webhooks in Node.js and Express.
  5. Prevent double-granting. Both the webhook and the callback can arrive for the same transaction. Use a database flag and check it before fulfilling.
  6. Use HTTPS in production. Paystack requires HTTPS for callback and webhook URLs.
  7. Switch keys. Replace sk_test_ with sk_live_ in your production environment variables.

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

  • ✓Express gives you full control over the payment flow. You call the Paystack REST API directly with fetch or axios from your route handlers.
  • ✓Your Paystack secret key must live in environment variables. Never hardcode it or commit it to version control.
  • ✓Amounts are in kobo (NGN), pesewas (GHS), or cents (ZAR/KES/USD). Multiply the display price by 100 before sending to Paystack.
  • ✓Always verify the transaction server-side after the redirect callback. The redirect alone is not proof of payment.
  • ✓Store the transaction reference in your database before redirecting so your webhook handler and callback handler can both find the correct order.
  • ✓No npm packages are required. The Paystack API is a plain REST API. Use the built-in fetch in Node 18+ or install node-fetch for older versions.

Frequently Asked Questions

Do I need to install a Paystack npm package?
No. The Paystack API is a standard REST API. Node 18+ includes a global fetch function you can use directly. Community packages like paystack-node exist but add unnecessary dependencies for most integrations.
Can I use Express with TypeScript for Paystack?
Yes. The Paystack API calls are the same. You would add type annotations to your request and response objects, but the fetch calls and JSON parsing are identical.
Why does my test payment return "Transaction not found"?
This usually means you initialized with a test key but are verifying with a live key, or vice versa. Make sure PAYSTACK_SECRET_KEY matches the mode you used. Test keys start with sk_test_ and live keys with sk_live_.
How do I handle multiple currencies in Express?
Pass the currency parameter when initializing the transaction. Paystack supports NGN, GHS, ZAR, KES, and USD. Your account must be activated for each currency you want to accept.
Should I use Express or NestJS for Paystack integration?
Express is simpler and has fewer abstractions. NestJS adds dependency injection, modules, and decorators which help on larger projects. The Paystack API calls are identical in both. Choose based on your project size and team preference.

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

Accept Payments with Paystack in Svelte and SvelteKit

Accept Paystack payments in SvelteKit using server endpoints for backend logic and Svelte components for the frontend. This guide covers initializing transactions, inline popup checkout, redirect checkout, and verification in a single full-stack SvelteKit project.

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

Understanding Kobo, Pesewa and Cents: Paystack Amount Handling

Paystack expects amounts in the smallest currency unit: kobo for Naira, pesewas for Cedis, cents for Rand, Shillings, and Dollars. This guide covers the conversion rules, the floating-point traps that cause wrong charges, and the helper functions that keep your amounts correct.

Read more

Transaction Not Found on Paystack Verify

Paystack returns "Transaction not found" when the reference you pass to the verify endpoint does not match any transaction in the current environment. The causes range from wrong reference strings to test/live key confusion to URL encoding problems. This guide covers every cause with specific fixes.

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.