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

Verify Paystack Payments in Node.js and Express

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.

In this article

  1. 1. Why Server-Side Verification Matters
  2. 2. Basic Verification Route
  3. 3. Amount and Currency Validation
  4. 4. Handling the Callback-Webhook Race
  5. 5. Retry Logic for Network Failures
  6. 6. Common Verification Mistakes
  7. 7. Ship Payments Faster

Why Server-Side Verification Matters

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.

Basic Verification Route

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.

Amount and Currency Validation

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.

Handling the Callback-Webhook Race

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:

  1. Both the callback route and the webhook handler call the same fulfillOrder function.
  2. fulfillOrder uses an atomic database update with a condition: only update if paid is still false.
  3. If the update affected zero rows, the order was already fulfilled. Return success without doing anything else.
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.

Retry Logic for Network Failures

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.

Common Verification Mistakes

  • Trusting the frontend amount. Never compare the Paystack amount against what the frontend sent. Compare it against what you stored in your database when you created the order.
  • Skipping currency checks. A customer could pay in a different currency than expected. If you expect NGN 5000 and they pay USD 50, the amounts might look similar but the value is very different.
  • Not URL-encoding the reference. If your reference contains special characters, the verify URL will break. Always use encodeURIComponent.
  • Mixing test and live keys. If you initialize with sk_test_ but verify with sk_live_, Paystack returns "Transaction not found." Both must be in the same mode.
  • Granting value before checking the database. Always check if the order exists and has not already been fulfilled before delivering value.

Ship Payments Faster

This guide is part of the Paystack integration guides by language and framework series. For the full payment flow, see Accept Payments with Paystack in Node.js and Express.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Verification is a single GET request to the Paystack API. No SDK is needed. Use the built-in fetch in Node 18+.
  • ✓Always check three things: transaction status equals "success", amount matches what you stored, and currency is correct.
  • ✓The redirect callback is not proof of payment. Someone could type the callback URL manually with a fake reference.
  • ✓Both the webhook and the redirect callback can arrive for the same transaction. Use a database flag to prevent double-granting.
  • ✓Wrap your fulfillment logic in a database transaction or use an atomic update to avoid race conditions.
  • ✓If verification fails due to a network error, retry with exponential backoff. The transaction state on Paystack does not change.

Frequently Asked Questions

Can I verify a transaction multiple times?
Yes. The verify endpoint is idempotent. It returns the same result every time for the same reference. Calling it multiple times does not change anything on Paystack side.
What happens if I verify a failed transaction?
Paystack returns the transaction data with a status of "failed" or "abandoned." Your code should check data.data.status and only fulfill when it equals "success."
How long can I verify a transaction after payment?
Paystack stores transaction data indefinitely. You can verify a transaction months or years after it was created. There is no expiration on the verify endpoint.
Should I verify in the webhook handler too?
Yes. Even though the webhook payload contains transaction data, you should call the verify endpoint to be certain. The webhook confirms the event happened, but verification gives you the authoritative transaction state directly from Paystack.

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

Currency Handling in Paystack: NGN, GHS, ZAR, KES and USD

Paystack supports NGN, GHS, ZAR, KES, and USD, but each currency comes with different available payment channels, settlement behaviors, and account requirements. This guide covers what you need to know to handle currencies correctly in your integration.

Read more

Paystack Duplicate Transaction Reference Error

Paystack returns "Duplicate Transaction Reference" when you send a reference that already exists. This guide covers every cause, from reused references to retry logic without regeneration, and gives you production-safe patterns for generating unique references every time.

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.