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. Payout Approval Workflows and Maker-Checker Controls
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 20, 2026

Payout Approval Workflows and Maker-Checker Controls

A maker-checker workflow requires two different people to process a payout: the maker creates the request, the checker reviews and approves it. Implement this with a payouts table that tracks maker_id and checker_id, a validation rule that prevents the same person from filling both roles, and optional threshold-based auto-approval for small amounts that do not warrant human review.

In this article

  1. 1. Why You Need Approval Controls
  2. 2. Implementing Maker-Checker
  3. 3. Threshold-Based Auto-Approval
  4. 4. Building the Audit Trail
  5. 5. Building the Approval Dashboard
  6. 6. Role-Based Access for Payout Operations
  7. 7. Build Trustworthy Payment Operations

Why You Need Approval Controls

When OTP is enabled on Paystack, every transfer has a built-in approval step: someone must enter the code. When you disable OTP for automation, that approval step disappears. Any valid API call moves money. Your code is the only thing between an approved payout and an unauthorized one.

Approval workflows replace OTP with something better: structured, auditable controls that fit your business process. Instead of a generic code sent to one phone number, you get specific approvals from named individuals with documented reasons and timestamps.

This matters for three reasons:

  • Fraud prevention: No single person can both create and execute a payout. Even if someone's account is compromised, they cannot drain funds without a second person's approval.
  • Error prevention: A second pair of eyes catches mistakes before money moves. Wrong amount, wrong recipient, wrong period.
  • Compliance: Financial regulators in most African markets expect businesses handling funds on behalf of others to have documented payment controls. An audit trail showing who approved what and when is your defense during audits.

Implementing Maker-Checker

// Database schema additions for approval workflow
// payout table fields:
// maker_id (integer, FK to admin_users)
// checker_id (integer, FK to admin_users, nullable)
// approved_at (timestamp, nullable)
// rejected_at (timestamp, nullable)
// rejection_reason (string, nullable)
// approval_ip (string, nullable)

// Maker creates a payout request
async function createPayout(makerId, vendorId, amount, reason) {
  var vendor = await db.vendors.findById(vendorId);

  var payout = await db.payouts.create({
    vendorId: vendorId,
    recipientCode: vendor.paystackRecipientCode,
    amountMinorUnit: amount,
    reason: reason,
    reference: 'payout_' + vendorId + '_' + Date.now(),
    status: 'pending_approval',
    makerId: makerId,
    createdAt: new Date(),
  });

  // Notify available checkers
  var checkers = await db.adminUsers.findByRole('checker');
  for (var i = 0; i < checkers.length; i++) {
    if (checkers[i].id !== makerId) {
      await notify(checkers[i].id, 'New payout awaiting approval: '
        + formatCurrency(amount) + ' to ' + vendor.businessName);
    }
  }

  return payout;
}

// Checker approves
async function approvePayout(payoutId, checkerId, ipAddress) {
  var payout = await db.payouts.findById(payoutId);

  if (payout.status !== 'pending_approval') {
    throw new Error('Payout is not pending approval');
  }
  if (payout.makerId === checkerId) {
    throw new Error('You cannot approve your own payout request');
  }

  await db.payouts.update(payoutId, {
    status: 'approved',
    checkerId: checkerId,
    approvedAt: new Date(),
    approvalIp: ipAddress,
  });

  // Log the approval action
  await db.auditLog.create({
    action: 'payout_approved',
    payoutId: payoutId,
    userId: checkerId,
    ipAddress: ipAddress,
    details: 'Approved payout of ' + payout.amountMinorUnit
      + ' to vendor ' + payout.vendorId,
    timestamp: new Date(),
  });
}

// Checker rejects
async function rejectPayout(payoutId, checkerId, reason) {
  var payout = await db.payouts.findById(payoutId);

  if (payout.status !== 'pending_approval') {
    throw new Error('Payout is not pending approval');
  }

  await db.payouts.update(payoutId, {
    status: 'rejected',
    checkerId: checkerId,
    rejectedAt: new Date(),
    rejectionReason: reason,
  });

  // Notify the maker
  await notify(payout.makerId,
    'Your payout request was rejected: ' + reason);
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Threshold-Based Auto-Approval

Not every payout needs human review. A 500 NGN vendor payout for a single order does not warrant the same scrutiny as a 5,000,000 NGN monthly settlement. Use thresholds to auto-approve low-risk payouts while routing high-value ones to human checkers.

var APPROVAL_THRESHOLDS = {
  NGN: {
    autoApprove: 5000000,      // Auto-approve below 50,000 NGN
    singleApproval: 50000000,   // Single checker up to 500,000 NGN
    dualApproval: Infinity,     // Two checkers above 500,000 NGN
  },
  KES: {
    autoApprove: 500000,        // Auto-approve below 5,000 KES
    singleApproval: 5000000,    // Single checker up to 50,000 KES
    dualApproval: Infinity,
  },
};

async function routePayoutForApproval(payoutId) {
  var payout = await db.payouts.findById(payoutId);
  var thresholds = APPROVAL_THRESHOLDS[payout.currency];

  if (!thresholds) {
    // Unknown currency, require manual approval
    return;
  }

  if (payout.amountMinorUnit <= thresholds.autoApprove) {
    await db.payouts.update(payoutId, {
      status: 'approved',
      checkerId: 'system_auto',
      approvedAt: new Date(),
    });
    await db.auditLog.create({
      action: 'payout_auto_approved',
      payoutId: payoutId,
      details: 'Amount below auto-approval threshold',
    });
  } else if (payout.amountMinorUnit <= thresholds.singleApproval) {
    // Single checker required
    await db.payouts.update(payoutId, {
      approvalLevel: 'single',
    });
  } else {
    // Dual checker required
    await db.payouts.update(payoutId, {
      approvalLevel: 'dual',
    });
  }
}

Review and adjust thresholds as your business grows. A startup might auto-approve everything below 10,000 NGN. A company processing millions daily might only auto-approve below 1,000 NGN. The right threshold depends on your risk tolerance and operational capacity.

Building the Audit Trail

Every payout action should be logged. Who created it, who approved it, who rejected it, when each action happened, and from which IP address. This audit trail serves three purposes: dispute resolution with vendors, internal investigations, and regulatory compliance.

// Audit log schema
// id (auto-increment)
// action (string: payout_created, payout_approved, payout_rejected,
//         payout_executed, payout_completed, payout_failed)
// payout_id (FK to payouts)
// user_id (FK to admin_users, nullable for system actions)
// ip_address (string)
// details (text)
// timestamp (datetime)

async function logPayoutAction(action, payoutId, userId, ipAddress, details) {
  await db.auditLog.create({
    action: action,
    payoutId: payoutId,
    userId: userId || 'system',
    ipAddress: ipAddress || 'internal',
    details: details,
    timestamp: new Date(),
  });
}

// Usage throughout the payout lifecycle
await logPayoutAction('payout_created', payout.id, makerId, req.ip,
  'Created payout of ' + amount + ' to vendor ' + vendorId);

await logPayoutAction('payout_approved', payout.id, checkerId, req.ip,
  'Approved by ' + checkerName);

await logPayoutAction('payout_executed', payout.id, null, null,
  'Sent to Paystack. Transfer code: ' + transferCode);

Store audit logs separately from the payouts table. They should be append-only (never update or delete). In a dispute or investigation, you need the complete history showing every action taken on a payout, in chronological order, with the responsible person identified.

Building the Approval Dashboard

Checkers need a clear view of what is waiting for their approval. Build a simple dashboard that shows pending payouts with enough context to make informed decisions.

The dashboard should display: the vendor name and business details, the payout amount and currency, the reason or description, the maker's name (who requested it), how long the payout has been waiting, the vendor's payout history (is this amount consistent with past payouts?), and action buttons for approve and reject.

Add quick filters: sort by amount (largest first for priority review), filter by currency, filter by maker (to review one person's batch). Include a summary at the top: total pending, total amount pending, number of payouts awaiting dual approval.

For the checker experience, speed matters. If approving a payout takes 30 seconds of clicking through screens, your checker will batch their reviews and your vendors wait longer. Make it possible to approve with one click after reviewing the details.

For the broader payout system architecture, see building an automated payout system on Paystack.

Role-Based Access for Payout Operations

Separate your payout operations into distinct roles with specific permissions.

Maker: Can create payout requests. Cannot approve or execute. Typically operations staff, account managers, or the system itself (for automated calculation of vendor earnings).

Checker: Can approve or reject payout requests. Cannot create or execute. Typically finance managers or senior operations staff. Must be a different person than the maker.

Executor: Can trigger the batch execution of approved payouts. In automated systems, this is a system process, not a person. In semi-manual systems, a senior finance person clicks "Run Payouts" after reviewing the approved batch.

Viewer: Can see payout status and history. Cannot take any action. Useful for support staff who need to answer vendor questions about payout status without having the ability to create or approve payouts.

Enforce these roles at the application level. Your API endpoints for payout creation, approval, and execution should check the user's role before allowing the action. Do not rely on hiding UI buttons as your only access control.

Build Trustworthy Payment Operations

Approval workflows are what make your payout system audit-ready and fraud-resistant. They are not exciting to build, but they are what separates a production system from a prototype.

The McTaba bootcamp covers payment operations and security as part of the payment integration module.

Create a free McTaba account

Key Takeaways

  • ✓Maker-checker is the gold standard for payout controls. One person creates the payout request, a different person approves it. Neither can do both.
  • ✓Threshold-based auto-approval reduces operational overhead. Small payouts below a set amount can be auto-approved, while large ones require human review.
  • ✓Every approval action should be logged: who approved, when, from which IP address. This audit trail is essential for dispute resolution and compliance.
  • ✓Role-based access ensures only authorized team members can create, approve, or execute payouts. Separate maker, checker, and executor roles.
  • ✓Build an approval dashboard that shows pending payouts with recipient details, amounts, and context so checkers can make informed decisions quickly.
  • ✓Multi-level approval chains (manager, then director) are warranted for very large payouts. Keep the chain short to avoid bottlenecks.

Frequently Asked Questions

Can the maker and checker be the same person for small payouts?
For auto-approved payouts below your threshold, the system acts as the checker. For manually approved payouts, you should always enforce that the maker and checker are different people, regardless of the amount. This is a fundamental principle of financial controls.
What if there is only one person on the team who can approve?
This is common in small startups. The pragmatic approach is to have the CEO or founder as the backup checker. Alternatively, use threshold-based auto-approval for routine payouts and only require manual approval for amounts above a reasonable threshold. As you grow, add more people to the checker role.
How long should payouts wait for approval before escalating?
Set an SLA for approval and escalate when it is breached. A common setup: payouts under 24 hours old are normal, payouts between 24-48 hours old trigger a reminder to available checkers, payouts over 48 hours old escalate to a manager. Vendors waiting too long for payout is a trust issue.

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 Vendor Payout Portal on Paystack

Your vendors need visibility into their earnings and payouts. This guide covers building a vendor-facing payout portal: an earnings dashboard showing pending and paid amounts, payout history with status tracking, self-service bank detail management, on-demand withdrawal requests, and the backend architecture to support it all.

Read more

Pending Transactions: Handling the Paystack Grey Zone

Not every Paystack transaction resolves instantly. Bank transfers, USSD payments, and mobile money can take seconds to minutes before reaching a final state. This guide covers how to handle transactions stuck in the grey zone between "paid" and "not paid" without losing money or frustrating customers.

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.