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. Building an Admin Payments Dashboard on the Paystack API
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Building an Admin Payments Dashboard on the Paystack API

Build your admin payments dashboard by combining data from your payments ledger (for fast queries) with real-time data from the Paystack API (for current status). Use your ledger for revenue summaries, refund totals, and fee calculations. Use the API for transaction search, customer lookup, and verifying individual payments. Cache API responses to avoid hitting rate limits.

In this article

  1. 1. Why Build Your Own Dashboard
  2. 2. Backend API Endpoints for the Dashboard
  3. 3. Real-Time Transaction Lookup
  4. 4. The Pending Transactions Monitor
  5. 5. Reconciliation Status Panel
  6. 6. Caching Paystack API Responses
  7. 7. Payment Channel Breakdown
  8. 8. Securing the Dashboard

Why Build Your Own Dashboard

Paystack provides a dashboard at dashboard.paystack.com. It shows transactions, settlements, and customers. It works well for quick lookups and basic monitoring. But it has limitations for day-to-day operations:

  • It does not show your application data. You cannot see which orders correspond to which payments, which users made which transactions, or which products generated the most revenue.
  • It does not show your internal statuses. A transaction might be "success" in Paystack but "pending fulfillment" in your system because your webhook handler crashed.
  • It does not aggregate the way your business needs. You might want revenue by product, by cohort, by referral source. Paystack only knows about the payment, not your business context.
  • Access control is limited. You might want your customer support team to see transaction details but not settlement data. Paystack dashboard roles may not match your internal roles.

A custom dashboard combines Paystack payment data with your application context. It is built on your payments ledger for fast queries and uses the Paystack API for real-time lookups.

Backend API Endpoints for the Dashboard

Your dashboard needs a handful of backend endpoints that query your ledger and the Paystack API. Here are the essential ones:

// routes/admin/payments.js
var express = require('express');
var router = express.Router();

// Middleware: require admin authentication
router.use(requireAdminAuth);

// Daily revenue summary
router.get('/api/admin/payments/summary', async function(req, res) {
  var days = parseInt(req.query.days) || 7;

  var result = await db.query(
    'SELECT '
    + '  DATE(created_at) as date, '
    + '  currency, '
    + '  COUNT(*) FILTER (WHERE event_type = 'charge.success') as charge_count, '
    + '  COALESCE(SUM(gross_amount) FILTER (WHERE event_type = 'charge.success'), 0) as gross_revenue, '
    + '  COALESCE(SUM(fee_amount) FILTER (WHERE event_type = 'charge.success'), 0) as total_fees, '
    + '  COALESCE(SUM(net_amount) FILTER (WHERE event_type = 'charge.success'), 0) as net_revenue, '
    + '  COUNT(*) FILTER (WHERE event_type = 'refund.processed') as refund_count, '
    + '  COALESCE(SUM(gross_amount) FILTER (WHERE event_type = 'refund.processed'), 0) as refund_total '
    + 'FROM payment_ledger '
    + 'WHERE created_at >= NOW() - INTERVAL '' + days + ' days' '
    + 'GROUP BY DATE(created_at), currency '
    + 'ORDER BY date DESC'
  );

  return res.json({ summary: result.rows });
});

// Transaction list with filters
router.get('/api/admin/payments/transactions', async function(req, res) {
  var page = parseInt(req.query.page) || 1;
  var perPage = Math.min(parseInt(req.query.perPage) || 50, 100);
  var offset = (page - 1) * perPage;

  var conditions = ['1=1'];
  var params = [];
  var paramIndex = 1;

  if (req.query.status) {
    conditions.push('o.status = $' + paramIndex);
    params.push(req.query.status);
    paramIndex++;
  }

  if (req.query.from) {
    conditions.push('l.created_at >= $' + paramIndex);
    params.push(req.query.from);
    paramIndex++;
  }

  if (req.query.to) {
    conditions.push('l.created_at <= $' + paramIndex);
    params.push(req.query.to);
    paramIndex++;
  }

  if (req.query.email) {
    conditions.push('l.customer_email ILIKE $' + paramIndex);
    params.push('%' + req.query.email + '%');
    paramIndex++;
  }

  params.push(perPage, offset);

  var result = await db.query(
    'SELECT l.reference, l.event_type, l.gross_amount, l.fee_amount, '
    + 'l.net_amount, l.currency, l.channel, l.customer_email, '
    + 'l.created_at, l.settled_at, o.status as order_status, o.id as order_id '
    + 'FROM payment_ledger l '
    + 'LEFT JOIN orders o ON o.id = l.order_id '
    + 'WHERE ' + conditions.join(' AND ') + ' '
    + 'ORDER BY l.created_at DESC '
    + 'LIMIT $' + paramIndex + ' OFFSET $' + (paramIndex + 1),
    params
  );

  return res.json({
    transactions: result.rows,
    page: page,
    perPage: perPage,
  });
});

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Real-Time Transaction Lookup

Sometimes your team needs to check a specific transaction's current status on Paystack, not just what your ledger recorded. This endpoint queries the Paystack API directly:

// Look up a transaction on Paystack in real time
router.get('/api/admin/payments/lookup/:reference', async function(req, res) {
  var reference = req.params.reference;

  // First check our ledger
  var ledgerResult = await db.query(
    'SELECT * FROM payment_ledger WHERE reference = $1 ORDER BY created_at',
    [reference]
  );

  // Then check Paystack
  var paystackResponse = await fetch(
    'https://api.paystack.co/transaction/verify/'
      + encodeURIComponent(reference),
    {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    }
  );

  var paystackData = await paystackResponse.json();

  // Check for discrepancies
  var discrepancy = null;
  if (ledgerResult.rows.length > 0 && paystackData.data) {
    var ledgerEntry = ledgerResult.rows[0];
    if (ledgerEntry.gross_amount !== paystackData.data.amount) {
      discrepancy = {
        type: 'amount_mismatch',
        ledger: ledgerEntry.gross_amount,
        paystack: paystackData.data.amount,
      };
    }
  } else if (ledgerResult.rows.length === 0 && paystackData.data
      && paystackData.data.status === 'success') {
    discrepancy = {
      type: 'missing_from_ledger',
      paystack_amount: paystackData.data.amount,
    };
  }

  return res.json({
    ledger: ledgerResult.rows,
    paystack: paystackData.data || null,
    discrepancy: discrepancy,
  });
});

The discrepancy detection is useful for support tickets. When a customer says "I paid but did not get my product," your support agent can look up the reference and immediately see whether the payment exists in Paystack, whether it was recorded in your ledger, and whether there is a mismatch.

The Pending Transactions Monitor

One of the most valuable dashboard panels shows orders that are stuck in a pending or processing state. These represent potential lost revenue or support issues.

// Pending transactions monitor
router.get('/api/admin/payments/pending', async function(req, res) {
  var result = await db.query(
    'SELECT o.id, o.paystack_reference, o.expected_amount, '
    + 'o.expected_currency, o.status, o.payment_channel, '
    + 'o.created_at, o.processing_started_at, '
    + 'EXTRACT(EPOCH FROM (NOW() - o.created_at)) / 60 as age_minutes '
    + 'FROM orders o '
    + 'WHERE o.status IN ($1, $2) '
    + 'ORDER BY o.created_at ASC',
    ['pending', 'processing']
  );

  // Group by urgency
  var urgent = [];
  var normal = [];

  for (var i = 0; i < result.rows.length; i++) {
    var row = result.rows[i];
    if (row.age_minutes > 60) {
      urgent.push(row);
    } else {
      normal.push(row);
    }
  }

  return res.json({
    total: result.rows.length,
    urgent: urgent,
    normal: normal,
  });
});

If the urgent list grows beyond a few entries, it is a red flag. Either your webhook endpoint is down, a specific payment channel is having issues, or there is a bug in your payment processing code. This panel should be the first thing your operations team checks every morning.

For strategies on handling pending transactions, see the pending transactions guide.

Reconciliation Status Panel

Show the results of your daily reconciliation job in the dashboard. This gives your team visibility into whether the books are clean.

// Reconciliation status
router.get('/api/admin/payments/reconciliation', async function(req, res) {
  var days = parseInt(req.query.days) || 14;

  var runs = await db.query(
    'SELECT run_date, settlement_date, matched_count, missing_count, '
    + 'mismatch_count, orphan_count, status '
    + 'FROM reconciliation_runs '
    + 'WHERE run_date >= NOW() - INTERVAL '' + days + ' days' '
    + 'ORDER BY run_date DESC'
  );

  var openIssues = await db.query(
    'SELECT ri.issue_type, ri.reference, ri.details, ri.created_at, '
    + 'rr.settlement_date '
    + 'FROM reconciliation_issues ri '
    + 'JOIN reconciliation_runs rr ON rr.id = ri.run_id '
    + 'WHERE ri.resolved = false '
    + 'ORDER BY ri.created_at DESC'
  );

  return res.json({
    runs: runs.rows,
    open_issues: openIssues.rows,
    last_clean_run: runs.rows.find(function(r) { return r.status === 'clean'; }),
  });
});

Highlight the last clean run date. If the last clean reconciliation was more than 3 days ago, the dashboard should show a warning. If it was more than 7 days ago, show an alert. Stale reconciliation means nobody is watching the books.

Caching Paystack API Responses

Dashboard pages get refreshed frequently. If every page load hits the Paystack API, you will burn through rate limits. Cache API responses for a short period:

// Simple in-memory cache
var cache = {};

async function cachedFetch(key, ttlSeconds, fetchFn) {
  var now = Date.now();
  var cached = cache[key];

  if (cached && (now - cached.timestamp) < ttlSeconds * 1000) {
    return cached.data;
  }

  var data = await fetchFn();
  cache[key] = { data: data, timestamp: now };
  return data;
}

// Usage in a dashboard endpoint
router.get('/api/admin/payments/paystack-balance', async function(req, res) {
  var balance = await cachedFetch('paystack_balance', 300, async function() {
    var response = await fetch('https://api.paystack.co/balance', {
      headers: {
        Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
      },
    });
    var data = await response.json();
    return data.data;
  });

  return res.json({ balance: balance });
});

Cache times depend on the data type. Balance and summary data can be cached for 5 minutes. Individual transaction lookups should not be cached (or cached for only 30 seconds) because support agents need current data. Settlement data can be cached for longer since it changes infrequently.

Payment Channel Breakdown

Understanding which payment channels your customers use helps you optimize your checkout flow and debug channel-specific issues:

// Payment channel breakdown
router.get('/api/admin/payments/channels', async function(req, res) {
  var days = parseInt(req.query.days) || 30;

  var result = await db.query(
    'SELECT '
    + '  channel, '
    + '  currency, '
    + '  COUNT(*) as transaction_count, '
    + '  SUM(gross_amount) as total_volume, '
    + '  SUM(fee_amount) as total_fees, '
    + '  ROUND(AVG(gross_amount)) as avg_transaction '
    + 'FROM payment_ledger '
    + 'WHERE event_type = $1 '
    + '  AND created_at >= NOW() - INTERVAL '' + days + ' days' '
    + 'GROUP BY channel, currency '
    + 'ORDER BY total_volume DESC',
    ['charge.success']
  );

  return res.json({ channels: result.rows });
});

If bank transfer volume is growing, you might want to optimize the bank transfer UX on your checkout page. If USSD volume is low, maybe you should remove it to simplify the checkout. If card failure rates are high (visible by comparing initialized transactions to successful ones), investigate whether specific card types or banks are causing issues.

Securing the Dashboard

Payment data is sensitive. Your admin dashboard must be protected:

// Admin authentication middleware
async function requireAdminAuth(req, res, next) {
  var token = req.headers.authorization;

  if (!token) {
    return res.status(401).json({ error: 'Authentication required' });
  }

  try {
    var user = await verifyAdminToken(token);

    if (!user || !user.is_admin) {
      return res.status(403).json({ error: 'Admin access required' });
    }

    // Log dashboard access for audit
    console.log(JSON.stringify({
      event: 'admin_dashboard_access',
      user_id: user.id,
      email: user.email,
      path: req.path,
      timestamp: new Date().toISOString(),
    }));

    req.adminUser = user;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}

Additional security measures:

  • Audit logging: Log every dashboard access with the user, the endpoint, and the timestamp. If payment data leaks, the audit log helps you trace what was accessed and by whom.
  • Role-based access: Not every admin needs to see everything. Support agents need transaction lookup but not settlement data. Finance needs settlement data but not customer details.
  • Rate limiting: Limit how many requests each admin can make per minute. This prevents both abuse and accidental infinite loops in dashboard code.
  • No Paystack secret key in the frontend. All Paystack API calls must go through your backend. The frontend sends requests to your admin API, which queries Paystack on behalf of the user.

If you want to learn how to build secure admin systems like this from the ground up, the McTaba 26-week bootcamp covers full-stack engineering including authentication, authorization, and production security patterns.

Key Takeaways

  • ✓Build your dashboard on top of your payments ledger for fast aggregation queries. Use the Paystack API for real-time lookups and transaction details.
  • ✓Essential dashboard views: daily revenue summary, transaction list with filters, refund tracker, pending transaction monitor, and reconciliation status.
  • ✓Cache Paystack API responses for 1-5 minutes to reduce API calls. Most dashboard data does not need real-time updates.
  • ✓Include a transaction search that queries both your ledger and the Paystack API. Your ledger covers recorded transactions; the API covers anything you might have missed.
  • ✓Add a pending transactions panel that shows orders stuck in processing state. This is your early warning system for webhook failures.
  • ✓Protect the dashboard behind authentication. Payment data is sensitive. Only authorized team members should have access.

Frequently Asked Questions

Should I use the Paystack API or my own database for dashboard data?
Use your database (payments ledger) for aggregation and historical queries. Use the Paystack API for real-time lookups and verifying individual transactions. Your database is faster for dashboards because it does not have API rate limits or network latency.
Can I embed the Paystack dashboard instead of building my own?
Paystack does not offer an embeddable dashboard widget. You need to build your own. The advantage is that your dashboard can combine payment data with your application data (orders, users, products) for a much richer view.
How do I handle multiple currencies in the dashboard?
Always display amounts with their currency code. Never sum amounts across different currencies. Show separate totals per currency. If you need a single "total revenue" number, convert to a base currency using a consistent exchange rate and label it clearly as a converted figure.
What frontend framework should I use for the dashboard?
Any framework works. React, Vue, plain HTML with JavaScript, even server-rendered pages. The backend API endpoints described in this guide return JSON. Any frontend can consume them. Choose whatever your team is comfortable with.
How often should dashboard data refresh?
The summary panel can refresh every 5 minutes. The pending transactions panel should refresh every 1-2 minutes since it is an early warning system. Individual transaction lookups should be real-time (no cache). Reconciliation status can refresh every 15 minutes.

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 Payment Verification and Reconciliation: Complete Guide

The complete engineering reference for verifying Paystack transactions server-side, preventing double grants, designing a payments ledger, reconciling settlements against your database, and building automated sweeper jobs that catch missing transactions.

Read more

Designing a Payments Ledger for a Paystack Integration

A payments ledger is your source of truth for every financial event in your Paystack integration. This guide walks through the database schema, write patterns, and query strategies that make reconciliation, refund tracking, and financial reporting reliable from day one.

Read more

Building a Daily Paystack Reconciliation Job

An automated reconciliation job runs every day, compares your database against Paystack, and alerts you when something does not match. This guide covers the full implementation: scheduling with cron, fetching and comparing transactions, handling edge cases, and sending alerts when issues are found.

Read more

Paystack Fees in Your Ledger: Gross vs Net Recording

When a customer pays NGN 50,000, Paystack takes a processing fee and settles the rest to your bank. How you record this in your ledger determines whether settlement reconciliation is straightforward or a nightmare. This guide covers the gross vs net recording decision and how to implement each approach.

Read more

Building Financial Reports from Paystack Data

Your accountant, your investors, and your tax authority all want financial reports. This guide shows how to build them from your payments ledger: revenue summaries, fee analysis, refund tracking, channel breakdowns, and cash flow projections, all driven by the data you already collect from Paystack.

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.