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