To verify a Paystack payment in Express, make a GET request to https://api.paystack.co/transaction/verify/:reference with your secret key in the Authorization header. Check that data.data.status is "success", that the amount matches what you stored, and that the currency is correct before granting value.
When a customer pays through Paystack, you get notified in two ways: the redirect callback (the customer's browser comes back to your site) and the webhook (Paystack calls your server directly). Neither should be trusted blindly.
The redirect is just a URL with a reference parameter. Anyone could type it in. The webhook is more reliable, but you still need to confirm the amount and currency match what you expected. Verification is the step where you call the Paystack API yourself and get the truth directly from the source.
Without verification, a customer could initialize a transaction for 100 NGN but your system might grant them a product worth 10,000 NGN. Verification closes that gap.
Create a route that takes a reference and calls the Paystack verify endpoint:
var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
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) {
return res.status(400).json({ error: 'Verification request failed' });
}
if (data.data.status !== 'success') {
return res.status(400).json({
error: 'Payment not successful',
paystack_status: data.data.status,
});
}
var amountInKobo = data.data.amount;
var currency = data.data.currency;
var customerEmail = data.data.customer.email;
res.json({
success: true,
reference: data.data.reference,
amount: amountInKobo / 100,
currency: currency,
email: customerEmail,
});
});
This is the minimum verification. It confirms the payment happened, but it does not check whether the amount matches your expected price. The next section adds that critical check.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
The most common verification mistake is skipping the amount check. Here is the pattern with full validation:
app.get('/api/verify/:reference', async function(req, res) {
var reference = req.params.reference;
// 1. Look up the order in your database
var order = await db.orders.findOne({ reference: reference });
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
if (order.paid) {
return res.json({ success: true, message: 'Already fulfilled' });
}
// 2. Call Paystack verify
var paystackRes = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{ headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET } }
);
var data = await paystackRes.json();
// 3. Check status
if (!data.status || data.data.status !== 'success') {
return res.status(400).json({ error: 'Payment not successful' });
}
// 4. Check amount (stored in kobo)
if (data.data.amount !== order.amountInKobo) {
console.error(
'Amount mismatch: expected ' + order.amountInKobo +
' got ' + data.data.amount
);
return res.status(400).json({ error: 'Amount mismatch' });
}
// 5. Check currency
if (data.data.currency !== order.currency) {
return res.status(400).json({ error: 'Currency mismatch' });
}
// 6. Fulfill the order (idempotently)
await db.orders.updateOne(
{ reference: reference, paid: false },
{ $set: { paid: true, paidAt: new Date() } }
);
res.json({ success: true });
});
The { reference: reference, paid: false } filter in the update query ensures that if two requests arrive simultaneously (webhook and callback), only one of them sets paid: true. The other finds no matching document and does nothing.
Both the redirect callback and the webhook can fire for the same successful transaction. They often arrive within milliseconds of each other. Your fulfillment logic must handle this gracefully.
The pattern:
fulfillOrder function.fulfillOrder uses an atomic database update with a condition: only update if paid is still false.async function fulfillOrder(reference) {
// Atomic update: only one caller wins
var result = await db.orders.updateOne(
{ reference: reference, paid: false },
{ $set: { paid: true, paidAt: new Date() } }
);
if (result.modifiedCount === 0) {
// Already fulfilled by the other handler
return { alreadyFulfilled: true };
}
// This is the winning handler. Deliver value.
var order = await db.orders.findOne({ reference: reference });
await sendConfirmationEmail(order.email, order);
// Unlock content, ship product, etc.
return { alreadyFulfilled: false };
}
Call this function from both your callback route and your webhook handler. The first one to arrive wins. The second one is a no-op.
If the fetch to Paystack's verify endpoint fails (network timeout, DNS issue, Paystack downtime), the transaction is still valid on Paystack's side. You can safely retry.
async function verifyWithRetry(reference, maxRetries) {
var retries = maxRetries || 3;
for (var attempt = 0; attempt < retries; attempt++) {
try {
var paystackRes = await fetch(
'https://api.paystack.co/transaction/verify/' + encodeURIComponent(reference),
{ headers: { Authorization: 'Bearer ' + PAYSTACK_SECRET } }
);
if (paystackRes.ok) {
return await paystackRes.json();
}
} catch (err) {
console.error('Verify attempt ' + (attempt + 1) + ' failed:', err.message);
}
// Exponential backoff: 1s, 2s, 4s
if (attempt < retries - 1) {
await new Promise(function(resolve) {
setTimeout(resolve, Math.pow(2, attempt) * 1000);
});
}
}
return null;
}
If all retries fail, log the reference and process it manually or queue it for background retry. The webhook will independently deliver the event, so the order can still be fulfilled through that path.
encodeURIComponent.sk_test_ but verify with sk_live_, Paystack returns "Transaction not found." Both must be in the same mode.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