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.
Before you start, make sure you have:
This guide uses plain JavaScript with CommonJS requires. The same patterns work in TypeScript or ESM imports.
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.
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.callback_url tells Paystack where to redirect after payment. We build that route next.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.
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.
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 });
});
.env and is read by Express.sk_test_ with sk_live_ in your production environment variables.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