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 a Vendor Payout Portal on Paystack
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Building a Vendor Payout Portal on Paystack

A vendor payout portal gives sellers visibility into their earnings and payout status. Build it with four components: an earnings dashboard (total earned, pending settlement, paid out), a payout history table (each payout with status, amount, date), bank detail management (view and update their payment details), and optionally, a withdrawal request feature for on-demand payouts.

In this article

  1. 1. What Vendors Need to See
  2. 2. Building the Earnings Dashboard
  3. 3. Payout History Table
  4. 4. Self-Service Bank Detail Management
  5. 5. On-Demand Withdrawal Requests
  6. 6. Showing Next Payout Information
  7. 7. Build Products Vendors Love

What Vendors Need to See

Vendors on your marketplace have one core question: where is my money? A good payout portal answers that question before they have to ask it.

The portal needs four sections:

  • Earnings summary: Total earned this period, total earned all time, amount pending (not yet paid out), amount available for withdrawal
  • Payout history: A table of past payouts with status, amount, date, and reference. Filterable by date range and status.
  • Bank details: Their current bank account on file, with an option to update it
  • Next payout: When the next scheduled payout will run and the estimated amount

Everything in this portal reads from your database, not from Paystack directly. Your webhook handlers keep payout statuses in sync, and your earnings calculation logic computes the amounts from order and transaction data. Paystack is the payment rail; your database is the display layer.

Building the Earnings Dashboard

async function getVendorEarnings(vendorId, periodStart, periodEnd) {
  // Orders completed in the period
  var orders = await db.orders.findCompletedByVendor(
    vendorId, periodStart, periodEnd
  );

  var grossEarnings = 0;
  var platformCommission = 0;

  for (var i = 0; i < orders.length; i++) {
    grossEarnings += orders[i].vendorAmount;
    platformCommission += orders[i].platformCommission;
  }

  // Payouts completed in the period
  var payouts = await db.payouts.findCompletedByVendor(
    vendorId, periodStart, periodEnd
  );
  var totalPaidOut = 0;
  for (var j = 0; j < payouts.length; j++) {
    totalPaidOut += payouts[j].amountMinorUnit;
  }

  // Payouts pending
  var pendingPayouts = await db.payouts.findPendingByVendor(vendorId);
  var totalPending = 0;
  for (var k = 0; k < pendingPayouts.length; k++) {
    totalPending += pendingPayouts[k].amountMinorUnit;
  }

  return {
    periodStart: periodStart,
    periodEnd: periodEnd,
    grossEarnings: grossEarnings,
    platformCommission: platformCommission,
    netEarnings: grossEarnings - platformCommission,
    paidOut: totalPaidOut,
    pendingPayout: totalPending,
    availableForWithdrawal: (grossEarnings - platformCommission) - totalPaidOut - totalPending,
    orderCount: orders.length,
  };
}

The availableForWithdrawal field is what the vendor cares about most. It shows how much money has accumulated from their sales that has not yet been paid out or is not already in a pending payout. This is the amount they could withdraw if you offer on-demand payouts.

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Payout History Table

async function getPayoutHistory(vendorId, page, perPage) {
  var payouts = await db.payouts.findByVendor(vendorId, {
    page: page || 1,
    perPage: perPage || 20,
    orderBy: 'created_at',
    orderDirection: 'DESC',
  });

  return payouts.map(function(p) {
    return {
      id: p.id,
      amount: p.amountMinorUnit / 100,
      currency: p.currency,
      status: mapStatusForDisplay(p.status),
      statusColor: getStatusColor(p.status),
      date: p.completedAt || p.initiatedAt || p.createdAt,
      reference: p.reference,
      reason: p.reason,
      bankName: p.bankName,
      accountEnding: p.accountNumber
        ? '****' + p.accountNumber.slice(-4)
        : null,
    };
  });
}

function mapStatusForDisplay(status) {
  var map = {
    pending_approval: 'Pending',
    approved: 'Scheduled',
    processing: 'Processing',
    completed: 'Paid',
    failed: 'Failed',
    reversed: 'Reversed',
    retry_scheduled: 'Retrying',
  };
  return map[status] || status;
}

function getStatusColor(status) {
  if (status === 'completed') return 'green';
  if (status === 'failed' || status === 'reversed') return 'red';
  if (status === 'processing') return 'yellow';
  return 'gray';
}

Show enough detail for the vendor to identify each payout without exposing internal system information. The reference, amount, date, and status are sufficient. Mask the bank account number (show only the last 4 digits) for security. If a payout failed, show a generic message like "Payment failed, will be retried" rather than the raw Paystack error message.

Self-Service Bank Detail Management

async function updateVendorBankDetails(vendorId, newBankCode, newAccountNumber) {
  // Step 1: Resolve the new account
  var resolved = await resolveAccount(newAccountNumber, newBankCode);
  if (!resolved) {
    throw new Error('Could not verify the bank account. Check the number and bank.');
  }

  // Step 2: Show the resolved name for confirmation
  // (Return this to the frontend, wait for user confirmation)
  return {
    needsConfirmation: true,
    resolvedAccountName: resolved.account_name,
    bankCode: newBankCode,
    accountNumber: newAccountNumber,
  };
}

async function confirmBankUpdate(vendorId, newBankCode, newAccountNumber, resolvedName) {
  // Step 3: Create new recipient on Paystack
  var newRecipient = await createRecipient(
    resolvedName, newAccountNumber, newBankCode
  );

  // Step 4: Update vendor record
  var oldRecipientCode = (await db.vendors.findById(vendorId)).paystackRecipientCode;

  await db.vendors.update(vendorId, {
    bankCode: newBankCode,
    accountNumber: newAccountNumber,
    resolvedAccountName: resolvedName,
    paystackRecipientCode: newRecipient.recipientCode,
    bankUpdatedAt: new Date(),
  });

  // Step 5: Log the change
  await db.auditLog.create({
    action: 'bank_details_updated',
    vendorId: vendorId,
    details: 'Old recipient: ' + oldRecipientCode
      + ', New recipient: ' + newRecipient.recipientCode,
  });
}

The two-step process (resolve, then confirm) is critical. It prevents vendors from accidentally entering wrong details. They see the resolved account name and confirm it is correct before the update takes effect. Any pending payouts continue using the old recipient code; only new payouts use the updated details.

On-Demand Withdrawal Requests

Some platforms let vendors request payouts on demand instead of waiting for the scheduled cycle. This is a premium feature that requires careful controls.

async function requestWithdrawal(vendorId, amount) {
  // Check available balance
  var earnings = await getVendorEarnings(vendorId);
  if (amount > earnings.availableForWithdrawal) {
    throw new Error(
      'Requested amount exceeds available balance. Available: '
      + (earnings.availableForWithdrawal / 100)
    );
  }

  // Check daily withdrawal limit
  var todayWithdrawals = await db.payouts.sumTodayByVendor(vendorId);
  var dailyLimit = 10000000; // 100,000 NGN
  if (todayWithdrawals + amount > dailyLimit) {
    throw new Error('Daily withdrawal limit reached');
  }

  // Check minimum withdrawal amount
  var minWithdrawal = 100000; // 1,000 NGN
  if (amount < minWithdrawal) {
    throw new Error('Minimum withdrawal is ' + (minWithdrawal / 100));
  }

  // Create the payout request
  var payout = await db.payouts.create({
    vendorId: vendorId,
    recipientCode: (await db.vendors.findById(vendorId)).paystackRecipientCode,
    amountMinorUnit: amount,
    reason: 'Vendor withdrawal request',
    reference: 'withdrawal_' + vendorId + '_' + Date.now(),
    status: 'approved', // Auto-approved for on-demand
    makerId: 'vendor_self',
    createdAt: new Date(),
  });

  // Execute immediately
  await processPayout(payout.id);

  return payout;
}

On-demand withdrawals need three safeguards: a maximum daily limit (prevents a compromised vendor account from draining their balance), a minimum amount (prevents micro-withdrawals that incur fees), and a verified available balance (ensures you do not pay out more than the vendor has earned).

Showing Next Payout Information

Vendors want to know when they will next be paid and how much. This reduces support inquiries and builds trust.

async function getNextPayoutInfo(vendorId) {
  var vendor = await db.vendors.findById(vendorId);
  var nextPayoutDate = getNextScheduledPayoutDate(vendor.payoutFrequency);

  var earnings = await getVendorEarnings(
    vendorId,
    getPayoutPeriodStart(nextPayoutDate),
    new Date()
  );

  return {
    nextPayoutDate: nextPayoutDate,
    estimatedAmount: earnings.availableForWithdrawal / 100,
    currency: vendor.currency,
    payoutMethod: vendor.paystackRecipientCode
      ? (vendor.recipientType === 'mpesa' ? 'M-Pesa' : 'Bank Transfer')
      : 'Not set up',
    bankName: vendor.bankName || null,
    accountEnding: vendor.accountNumber
      ? '****' + vendor.accountNumber.slice(-4)
      : null,
  };
}

Show this prominently on the vendor dashboard. "Your next payout of approximately 45,000 KES is scheduled for Wednesday, July 23." That one sentence answers the question the vendor would otherwise email your support team about.

For the full automated payout system that powers this portal, see building an automated payout system on Paystack.

Build Products Vendors Love

A good payout portal is the difference between vendors who trust your platform and vendors who are constantly worried about their money. Build it well, and it becomes a competitive advantage.

The McTaba bootcamp covers building full-stack marketplace products, including the vendor-facing tools that keep sellers engaged.

Create a free McTaba account

Key Takeaways

  • ✓Vendors need four things: how much they have earned, how much has been paid out, when the next payout is coming, and the ability to update their bank details.
  • ✓Build an earnings dashboard that shows total earnings, pending settlement, and paid out amounts per period.
  • ✓Payout history should show each transfer with its status (processing, completed, failed), amount, date, and reference.
  • ✓Self-service bank detail updates reduce support tickets. Let vendors change their bank through the portal with proper validation.
  • ✓On-demand withdrawals are a premium feature. Implement daily limits and balance checks before allowing instant payouts.
  • ✓The portal reads from your database, not from Paystack directly. Keep your payout records synchronized via webhooks.

Frequently Asked Questions

Should the portal show real-time data from Paystack?
No. The portal should read from your database, which is kept in sync with Paystack through webhooks. Calling Paystack APIs on every vendor page load would be slow, unreliable (if Paystack is slow, your portal is slow), and unnecessary. Keep your local data accurate through webhooks and reconciliation.
How should I handle showing failed payouts to vendors?
Keep the message simple and actionable. "Your payout could not be processed. Our team is looking into it." or "Your bank details may need updating. Please verify your account information." Do not show raw Paystack error messages, which can be confusing or alarming.
Should I let vendors see the platform commission on each order?
Yes. Transparency builds trust. Show the gross order amount, the platform commission, and the net amount the vendor receives. Vendors who understand the commission structure are less likely to dispute payouts.

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 Transfers and Payouts: Complete Guide

Everything you need to send money out of your Paystack account: creating transfer recipients, initiating single and bulk transfers, handling OTP requirements, managing failures, and building automated payout systems for bank accounts, M-Pesa wallets, and mobile money across Africa.

Read more

Transfer OTP: Enabling, Disabling and Automating

Paystack requires OTP confirmation for transfers by default. This guide explains the OTP flow, how to disable it for automated payout systems, the security trade-offs of disabling OTP, and how to build your own application-level approval controls to replace OTP with something better suited to production automation.

Read more

Funding Your Paystack Balance Programmatically

Your Paystack balance needs funds before you can send transfers. This guide covers the different ways to fund your balance, setting up dedicated virtual accounts, monitoring balance levels, building automated funding alerts, and integrating balance management into your payout pipeline so you never run out mid-payout.

Read more

Building a Driver Payout System for a Ride Hailing App

Ride-hailing drivers need fast, reliable payouts. This guide covers building a driver payout system on Paystack: calculating trip-based earnings with commission deductions, daily settlement cycles, M-Pesa and bank payout options, handling cash-collected trips, real-time driver balance tracking, and instant withdrawal features.

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

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.