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. Build a Freelance Escrow Style Platform
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Freelance Escrow Style Platform

Build a freelance escrow platform by collecting milestone payments via Paystack (funds land in your Paystack balance), holding them in a "pending release" state in your database, and transferring to the freelancer's registered bank account via the Paystack Transfers API only when the client explicitly approves the deliverable.

In this article

  1. 1. Milestone Setup and Payment Collection
  2. 2. Work Approval and Freelancer Payout
  3. 3. Learn More

Milestone Setup and Payment Collection

Tables: projects (client_id, freelancer_id, title, status), milestones (project_id, title, amount, status, paystack_ref, funded_at, released_at), freelancers (user_id, name, bank_code, account_number, paystack_recipient_code).

var PLATFORM_FEE_RATE = 0.08; // 8%

async function fundMilestone(clientEmail, milestoneId) {
  var milestone = await db.milestones.findById(milestoneId);
  if (milestone.status !== 'awaiting_funding') throw new Error('Milestone not ready to fund');

  var reference = 'MILE-' + milestoneId + '-' + Date.now();
  var res = await fetch('https://api.paystack.co/transaction/initialize', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email: clientEmail,
      amount: milestone.amount,
      currency: 'NGN',
      reference,
      metadata: { milestone_id: milestoneId, purpose: 'milestone_funding' },
    }),
  });
  await db.milestones.update(milestoneId, { paystack_ref: reference });
  return (await res.json()).data.authorization_url;
}

// Webhook: mark milestone as funded
if (event.event === 'charge.success' && event.data.metadata.purpose === 'milestone_funding') {
  var milestoneId = event.data.metadata.milestone_id;
  await db.milestones.update(milestoneId, { status: 'funded', funded_at: new Date() });
  // Notify freelancer: "Milestone funded — you can start work"
  var milestone = await db.milestones.findById(milestoneId);
  await notifyFreelancer(milestone.project_id, 'Milestone "' + milestone.title + '" funded. Start working!');
}

Work Approval and Freelancer Payout

// Client approves work — release funds to freelancer
async function approveMilestone(clientId, milestoneId) {
  var milestone = await db.milestones.findById(milestoneId);
  var project = await db.projects.findById(milestone.project_id);
  if (project.client_id !== clientId) throw new Error('Not your project');
  if (milestone.status !== 'funded') throw new Error('Milestone not funded or already released');

  var freelancer = await db.freelancers.findByUser(project.freelancer_id);

  // Register recipient if not yet done
  if (!freelancer.paystack_recipient_code) {
    var rcpRes = await fetch('https://api.paystack.co/transferrecipient', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ type: 'nuban', name: freelancer.name, account_number: freelancer.account_number, bank_code: freelancer.bank_code, currency: 'NGN' }),
    });
    var code = (await rcpRes.json()).data.recipient_code;
    await db.freelancers.update(freelancer.id, { paystack_recipient_code: code });
    freelancer.paystack_recipient_code = code;
  }

  // Deduct platform fee
  var platformFee = Math.floor(milestone.amount * PLATFORM_FEE_RATE);
  var payout = milestone.amount - platformFee;

  await fetch('https://api.paystack.co/transfer', {
    method: 'POST',
    headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      source: 'balance',
      amount: payout,
      recipient: freelancer.paystack_recipient_code,
      reason: 'Milestone payout: ' + milestone.title,
      metadata: { milestone_id: milestoneId },
    }),
  });

  await db.milestones.update(milestoneId, { status: 'released', released_at: new Date() });
  await notifyFreelancer(project.freelancer_id, 'Payment released: NGN ' + (payout / 100));
}

// Dispute: admin can refund to client or release to freelancer
async function resolveDispute(milestoneId, decision) {
  if (decision === 'refund_client') {
    var milestone = await db.milestones.findById(milestoneId);
    var project = await db.projects.findById(milestone.project_id);
    var client = await db.clients.findById(project.client_id);
    await fetch('https://api.paystack.co/refund', {
      method: 'POST',
      headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
      body: JSON.stringify({ transaction: milestone.paystack_ref }),
    });
    await db.milestones.update(milestoneId, { status: 'refunded' });
  } else if (decision === 'release_freelancer') {
    await approveMilestone('admin', milestoneId); // admin overrides
  }
}

Get weekly developer tips

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

No spam. Unsubscribe anytime.

Learn More

See build a tutoring marketplace with split payments for a related platform model using Paystack Subaccounts instead of manual transfers.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Funds land in your Paystack balance — you hold them on behalf of the client until work is approved.
  • ✓Structure projects as milestones: client pays per milestone, not the whole project upfront.
  • ✓Release funds via Paystack Transfers only after explicit client approval — never automatically.
  • ✓Register freelancers as Paystack Transfer Recipients at onboarding so payouts are instant.
  • ✓Charge a platform fee (5-10%) at release by transferring less than the full milestone amount.

Frequently Asked Questions

Can Paystack legally hold funds in escrow for me?
Paystack is not an escrow provider — it is a payment processor. When clients pay, the funds land in your Paystack settlement balance. You are technically holding the funds on behalf of both parties. This is fine for short-term holds (days to weeks) but get legal advice if your platform holds funds for extended periods. For formal escrow in Nigeria, you would need a licensed escrow agent.
What happens if the client never approves the work?
Set a maximum hold period (e.g., 14 days after the freelancer marks work as delivered). If the client does not respond within 14 days, auto-release the funds to the freelancer. The client's failure to review within the window counts as implicit approval. State this policy clearly in your terms of service.
How do I protect the freelancer from a client who keeps requesting revisions?
Specify a revision limit in the milestone scope (e.g., 2 revisions included). After the limit, additional revisions require a new milestone payment. Include the scope and revision count in the milestone description field so both parties agree before the client funds the milestone.

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

Things You Can Build with Paystack: Project Guides

A complete map of real-world projects you can build using the Paystack API. From online store checkouts and subscription platforms to savings apps and solar pay-as-you-go systems, each category links to step-by-step build guides with working code.

Read more

Build a Parking Payment App

A build guide for a parking payment app where drivers scan a QR code on entry, pay for their session via Paystack (card or M-Pesa), and the system validates payment before the barrier lifts for exit. Covers time-based pricing, session management, and overstay handling.

Read more

Build a Referral Payout System

A build guide for a referral payout system where users share referral links, earn commissions when referrals convert, and withdraw their balance via Paystack Transfers to their bank account or M-Pesa wallet.

Read more

Build a Farmers Market Ordering Platform

A build guide for a farmers market ordering platform where customers order fresh produce from multiple farmers in one cart, the platform collects the combined payment via Paystack, and each farmer receives their share via Paystack Split Payments or Transfers. Covers inventory management, order routing to individual farmers, and delivery coordination.

Read more

USSD Plus Paystack: Reaching Feature Phone Users in Kenya

Millions of Kenyans still use feature phones. They cannot open a Paystack checkout page, scan a QR code, or download your app. But they can navigate a USSD menu and respond to an SMS. This guide shows you how to combine a USSD menu system (built with Africa's Talking or similar providers) with Paystack as the payment backend, bridging the gap between old hardware and modern payment infrastructure.

Read more

Who Bears the Fee: Split Payment Fee Configuration

A detailed breakdown of how the Paystack processing fee is handled in split payments. Covers the three bearer options (account, subaccount, all), worked examples showing the exact settlement amounts under each option, unit economics analysis, and how to choose the right fee configuration for your marketplace.

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.