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 Real Estate Viewing Deposit System
Bonaventure OgetoBy Bonaventure Ogeto|Updated July 22, 2026

Build a Real Estate Viewing Deposit System

Build a real estate viewing deposit system by requiring a refundable deposit via Paystack before confirming a property viewing slot. Generate a unique viewing pass on the charge.success webhook and send it to the prospective buyer. After the viewing, refund the deposit via the Paystack Refund API if the buyer attended. Forfeit the deposit for no-shows, compensating the agent via Paystack Transfers.

In this article

  1. 1. Viewing Booking and Deposit Collection
  2. 2. Attendance Confirmation, Refund, and No-Show Handling
  3. 3. Learn More

Viewing Booking and Deposit Collection

Tables: properties (title, address, type, price, bedrooms, agent_id, status, images[]), agents (name, phone, email, paystack_recipient_code), viewings (property_id, prospect_email, prospect_phone, prospect_name, scheduled_at, deposit_amount, status, viewing_code, paystack_ref, attended_at, refunded_at).

var VIEWING_DEPOSIT = 300000; // NGN 3,000

async function bookViewing(prospectEmail, prospectData, propertyId, scheduledAt) {
  var property = await db.properties.findById(propertyId);
  if (property.status !== 'available') throw new Error('Property is no longer available for viewing');

  // Check no conflicts for this slot
  var conflicts = await db.viewings.findConflicts(propertyId, scheduledAt);
  if (conflicts.length >= 3) throw new Error('This slot is fully booked — choose another time');

  var reference = 'VIEW-' + propertyId + '-' + Date.now();

  var viewing = await db.viewings.create({
    property_id: propertyId,
    prospect_email: prospectEmail,
    prospect_name: prospectData.name,
    prospect_phone: prospectData.phone,
    scheduled_at: scheduledAt,
    deposit_amount: VIEWING_DEPOSIT,
    status: 'pending_payment',
    paystack_ref: reference,
  });

  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: prospectEmail,
      amount: VIEWING_DEPOSIT,
      currency: 'NGN',
      reference,
      metadata: { viewing_id: viewing.id, property_id: propertyId, prospect_phone: prospectData.phone },
    }),
  });
  return (await res.json()).data.authorization_url;
}

// Webhook: confirm viewing and issue viewing code
if (event.event === 'charge.success') {
  var meta = event.data.metadata;
  if (!meta.viewing_id) return res.sendStatus(200);

  var viewingCode = 'VW-' + Math.random().toString(36).toUpperCase().slice(2, 9);
  await db.viewings.update(meta.viewing_id, { status: 'confirmed', viewing_code: viewingCode });

  var viewing = await db.viewings.findById(meta.viewing_id);
  var property = await db.properties.findById(meta.property_id);
  await sendViewingConfirmation(viewing.prospect_email, viewing.prospect_name, property, viewing.scheduled_at, viewingCode);
}

Attendance Confirmation, Refund, and No-Show Handling

// Agent marks prospect as attended — trigger refund
async function confirmAttendance(agentId, viewingId) {
  var viewing = await db.viewings.findById(viewingId);
  var property = await db.properties.findById(viewing.property_id);
  if (property.agent_id !== agentId) throw new Error('Not your listing');
  if (viewing.status !== 'confirmed') throw new Error('Viewing not in confirmed state');

  // Refund the deposit in full
  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: viewing.paystack_ref }),
  });

  await db.viewings.update(viewingId, {
    status: 'attended',
    attended_at: new Date(),
    refunded_at: new Date(),
  });
  await sendRefundNotification(viewing.prospect_email, viewing.deposit_amount / 100);
}

// Agent marks no-show — forfeit deposit
async function markNoShow(agentId, viewingId) {
  var viewing = await db.viewings.findById(viewingId);
  var property = await db.properties.findById(viewing.property_id);
  if (property.agent_id !== agentId) throw new Error('Not your listing');

  await db.viewings.update(viewingId, { status: 'no_show' });

  // Pay 60% of forfeited deposit to the agent as compensation
  var agent = await db.agents.findById(property.agent_id);
  var agentShare = Math.floor(viewing.deposit_amount * 0.60);

  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: agentShare,
      recipient: agent.paystack_recipient_code,
      reason: 'No-show compensation: viewing #' + viewingId,
    }),
  });
  await sendNoShowNotification(viewing.prospect_phone);
}

// Viewing performance dashboard for agents
async function getAgentViewingStats(agentId, month) {
  var viewings = await db.viewings.findByAgentAndMonth(agentId, month);
  return {
    total_viewings: viewings.length,
    attended: viewings.filter(function(v) { return v.status === 'attended'; }).length,
    no_shows: viewings.filter(function(v) { return v.status === 'no_show'; }).length,
    compensation_earned: viewings
      .filter(function(v) { return v.status === 'no_show'; })
      .reduce(function(sum, v) { return sum + Math.floor(v.deposit_amount * 0.60); }, 0),
  };
}

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 hotel booking and deposit system for a related deposit collection pattern with a longer hold window and balance at check-in.

Sign up for the McTaba newsletter

Key Takeaways

  • ✓Viewing deposits filter out time-wasters — only serious buyers commit to a small, refundable fee.
  • ✓Generate a unique viewing confirmation code in the charge.success webhook.
  • ✓Refund deposits in full via Paystack Refund API when the buyer attends as confirmed by the agent.
  • ✓Forfeit deposits on no-shows and pay the agent a portion as compensation for their wasted time.
  • ✓Track viewing-to-offer conversion rate per property to measure the quality of your leads.

Frequently Asked Questions

Is charging a viewing deposit legal in Nigeria and Kenya?
There is no law prohibiting viewing fees as long as the refund policy is clearly stated upfront and honored. In practice, most established real estate agencies do not charge viewing fees — it is more common among high-demand rental markets and property platforms trying to reduce no-shows. Disclose the fee and refund terms prominently before the prospect pays to avoid disputes.
What if the property is let before the booked viewing date?
Cancel all pending viewings and refund all deposits in full via the Paystack Refund API. Send an apology email to each prospect with information about similar properties. Update the property status to "let" to prevent new bookings. Quick full refunds protect your reputation — delays or partial refunds cause lasting damage to buyer trust.
How do I prevent agents from incorrectly marking attendees as no-shows to collect compensation?
Require the prospect to confirm attendance as well — send them an SMS with a confirmation link after the viewing. Both the agent and the prospect must confirm for the viewing to be marked "attended" and the refund triggered. If only the agent marks no-show but the prospect disputes it, flag for manual review. This two-sided confirmation prevents fraud from either party.

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 Veterinary Clinic Booking and Payment System

A build guide for a veterinary clinic booking and payment system where pet owners book appointments online, pay a deposit via Paystack to confirm the slot, and pay the balance at the clinic after the consultation. Includes pet health records, vaccination schedules, and automated reminders.

Read more

Running Paystack and Daraja Side by Side

Some Kenyan products need both Paystack and Daraja. Cards and Pesalink through Paystack, direct M-Pesa through Daraja. This guide covers the architecture: when to route where, shared database design, unified webhook handling, and reconciliation across providers.

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.