Building a Rent Collection Platform for Kenyan Landlords
Build a rent collection platform by registering properties with unit details and rent amounts, linking tenants to units, and collecting monthly rent through Paystack via M-Pesa or card. Use Paystack Splits to automatically divide each payment between the landlord and the property agent. Send automated reminders before the due date and track late payments with configurable penalty rules. Disburse landlord funds via Paystack Transfers to their bank account or M-Pesa wallet.
How Rent Collection Works in Kenya Today
The typical Kenyan rent collection process looks like this: the landlord or caretaker has a notebook. The tenant sends M-Pesa to the landlord's personal number or a paybill. The tenant sends a screenshot of the M-Pesa confirmation. The caretaker marks it in the notebook. At the end of the month, the caretaker reports to the landlord (or the property agent) with a list of who paid and who did not.
This works for a landlord with five units. It falls apart at twenty. And it is a mess at fifty or a hundred. Common problems:
- No clear record. Disputes about whether rent was paid are common. The tenant says they paid. The landlord says they did not. The M-Pesa statement shows a payment to a personal number with no label.
- Agent commissions are manual. The property agent takes their 10% (or whatever the agreed rate) and sends the rest to the landlord. This math is done on paper or in the agent's head. Errors and disputes follow.
- Late penalties are inconsistent. The lease says KES 500 per day after the 5th. In practice, nobody tracks this. Some tenants get penalized. Others do not. The inconsistency creates resentment.
- No vacancy tracking. The agent manages 15 buildings. Which units are vacant? For how long? What is the lost revenue? This data does not exist in a notebook system.
A rent collection platform fixes all of this by digitizing the entire cycle: tenant registration, monthly billing, payment collection via Paystack, automatic commission splits, receipts, and disbursement to landlords.
Data Model: Properties, Units, and Tenants
// Properties (buildings)
const createPropertiesTable = `
CREATE TABLE properties (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
location VARCHAR(200) NOT NULL,
county VARCHAR(50),
landlord_id UUID REFERENCES landlords(id),
agent_id UUID REFERENCES agents(id),
agent_commission_percent INT DEFAULT 10, -- Agent commission as percentage
rent_due_day INT DEFAULT 1, -- Day of month rent is due
late_penalty_type VARCHAR(20) DEFAULT 'flat', -- 'flat', 'daily', 'none'
late_penalty_amount INT DEFAULT 0, -- In cents
late_penalty_grace_days INT DEFAULT 5, -- Days after due before penalty
total_units INT DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW()
);
`;
// Units within a property
const createUnitsTable = `
CREATE TABLE units (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
property_id UUID REFERENCES properties(id),
unit_number VARCHAR(20) NOT NULL, -- 'A1', 'G5', 'Flat 12'
unit_type VARCHAR(50), -- 'bedsitter', 'one_bedroom', 'two_bedroom', 'shop'
rent_amount INT NOT NULL, -- Monthly rent in cents
deposit_amount INT, -- Security deposit
status VARCHAR(20) DEFAULT 'vacant', -- 'occupied', 'vacant', 'maintenance'
tenant_id UUID REFERENCES tenants(id),
lease_start DATE,
lease_end DATE,
UNIQUE(property_id, unit_number)
);
`;
// Tenants
const createTenantsTable = `
CREATE TABLE tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(15) NOT NULL,
email VARCHAR(200),
id_number VARCHAR(20),
emergency_contact VARCHAR(15),
created_at TIMESTAMP DEFAULT NOW()
);
`;
// Landlords
const createLandlordsTable = `
CREATE TABLE landlords (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
phone VARCHAR(15) NOT NULL,
email VARCHAR(200),
bank_name VARCHAR(100),
bank_account VARCHAR(30),
bank_code VARCHAR(20),
mpesa_number VARCHAR(15), -- Alternative: settle to M-Pesa
preferred_settlement VARCHAR(20) DEFAULT 'bank', -- 'bank' or 'mpesa'
paystack_recipient_code VARCHAR(50),
created_at TIMESTAMP DEFAULT NOW()
);
`;
// Agents
const createAgentsTable = `
CREATE TABLE agents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(200) NOT NULL,
phone VARCHAR(15) NOT NULL,
email VARCHAR(200),
bank_account VARCHAR(30),
bank_code VARCHAR(20),
paystack_recipient_code VARCHAR(50),
paystack_subaccount_code VARCHAR(50), -- For Paystack Splits
created_at TIMESTAMP DEFAULT NOW()
);
`;
// Monthly rent invoices
const createRentInvoicesTable = `
CREATE TABLE rent_invoices (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
unit_id UUID REFERENCES units(id),
tenant_id UUID REFERENCES tenants(id),
month VARCHAR(7) NOT NULL, -- '2026-07'
rent_amount INT NOT NULL,
penalty_amount INT DEFAULT 0,
total_amount INT NOT NULL, -- rent + penalty
paid_amount INT DEFAULT 0,
balance INT GENERATED ALWAYS AS (total_amount - paid_amount) STORED,
status VARCHAR(20) DEFAULT 'unpaid', -- 'unpaid', 'partial', 'paid', 'overdue'
due_date DATE NOT NULL,
paid_date TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(unit_id, month)
);
`;
Key design points:
- Agent commission on the property, not the platform. Different landlords may negotiate different commission rates with their agents. Store it per property.
- Late penalty configuration per property. Some landlords charge KES 500 flat after the 5th. Others charge KES 100 per day. Others charge nothing. The flexibility matters because Kenyan landlords have widely varying lease terms.
- Agent subaccount code. This is for Paystack Splits. The agent is registered as a Paystack subaccount, and their commission is routed to them automatically when rent is collected.
Monthly Rent Invoice Generation
On the 25th of each month (or a configurable date), the system generates rent invoices for the next month. This gives the tenant a few days to prepare their payment before the due date.
// Scheduled job: generate next month's rent invoices
async function generateMonthlyInvoices() {
const nextMonth = getNextMonthString(); // e.g., '2026-08'
// Get all occupied units
const occupiedUnits = await db.query(
`SELECT u.*, p.rent_due_day, t.phone, t.first_name, t.last_name
FROM units u
JOIN properties p ON u.property_id = p.id
JOIN tenants t ON u.tenant_id = t.id
WHERE u.status = 'occupied'`
);
for (const unit of occupiedUnits) {
// Check if invoice already exists for this month
const existing = await db.query(
'SELECT id FROM rent_invoices WHERE unit_id = $1 AND month = $2',
[unit.id, nextMonth]
);
if (existing.length > 0) continue;
const dueDate = new Date(
parseInt(nextMonth.split('-')[0]),
parseInt(nextMonth.split('-')[1]) - 1,
unit.rent_due_day
);
await db.insert('rent_invoices', {
unit_id: unit.id,
tenant_id: unit.tenant_id,
month: nextMonth,
rent_amount: unit.rent_amount,
penalty_amount: 0,
total_amount: unit.rent_amount,
due_date: dueDate,
status: 'unpaid',
});
// Send SMS notification
const rentKES = (unit.rent_amount / 100).toLocaleString();
await sendSMS(
unit.phone,
`Rent invoice for ${nextMonth}: KES ${rentKES} for unit ${unit.unit_number}. Due date: ${dueDate.toLocaleDateString('en-KE')}. Pay via the portal or call ${process.env.PLATFORM_PHONE}.`
);
}
}
// Scheduled job: apply late penalties
async function applyLatePenalties() {
const today = new Date();
const overdueInvoices = await db.query(
`SELECT ri.*, u.property_id, u.unit_number,
p.late_penalty_type, p.late_penalty_amount, p.late_penalty_grace_days
FROM rent_invoices ri
JOIN units u ON ri.unit_id = u.id
JOIN properties p ON u.property_id = p.id
WHERE ri.status IN ('unpaid', 'partial', 'overdue')
AND ri.due_date < $1
AND p.late_penalty_type != 'none'`,
[today]
);
for (const invoice of overdueInvoices) {
const daysPastDue = Math.floor(
(today.getTime() - new Date(invoice.due_date).getTime()) / (1000 * 60 * 60 * 24)
);
if (daysPastDue <= invoice.late_penalty_grace_days) continue;
let penaltyAmount = 0;
if (invoice.late_penalty_type === 'flat') {
// One-time flat penalty (only apply once)
penaltyAmount = invoice.penalty_amount === 0 ? invoice.late_penalty_amount : 0;
} else if (invoice.late_penalty_type === 'daily') {
// Daily penalty: recalculate based on days past grace period
const penaltyDays = daysPastDue - invoice.late_penalty_grace_days;
penaltyAmount = (invoice.late_penalty_amount * penaltyDays) - invoice.penalty_amount;
}
if (penaltyAmount > 0) {
await db.query(
`UPDATE rent_invoices
SET penalty_amount = penalty_amount + $1,
total_amount = rent_amount + penalty_amount + $1,
status = 'overdue'
WHERE id = $2`,
[penaltyAmount, invoice.id]
);
}
}
}
The penalty logic must be transparent to tenants. When a penalty is applied, send an SMS: "Late penalty of KES X applied to your rent for Unit Y. Total now due: KES Z." Surprise penalties cause disputes. Transparent penalties, while not popular, are at least defensible.
Collecting Rent with Paystack Splits
Paystack Splits let you divide a single payment between multiple recipients automatically. For rent collection, this means the tenant pays once and the money is split between the landlord and the agent. No manual calculation. No waiting for the agent to forward the landlord's share.
First, register the agent as a Paystack subaccount:
// Register agent as Paystack subaccount (do this once)
async function createAgentSubaccount(agent) {
const response = await fetch('https://api.paystack.co/subaccount', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
business_name: agent.name,
settlement_bank: agent.bank_code,
account_number: agent.bank_account,
percentage_charge: 10, // Default, overridden per transaction
}),
});
const data = await response.json();
return data.data.subaccount_code;
}
Then, when collecting rent, include the split information:
// Collect rent with automatic agent commission split
app.post('/api/rent/pay', async (req, res) => {
const { invoiceId } = req.body;
const invoice = await db.query(
`SELECT ri.*, u.unit_number, u.property_id,
t.phone, t.email, t.first_name, t.last_name,
p.agent_commission_percent, p.name as property_name,
a.paystack_subaccount_code
FROM rent_invoices ri
JOIN units u ON ri.unit_id = u.id
JOIN tenants t ON ri.tenant_id = t.id
JOIN properties p ON u.property_id = p.id
LEFT JOIN agents a ON p.agent_id = a.id
WHERE ri.id = $1`,
[invoiceId]
);
const reference = `RENT-${invoice[0].unit_number}-${invoice[0].month}-${Date.now()}`;
const chargePayload = {
email: invoice[0].email || `${invoice[0].phone}@rent.portal`,
amount: invoice[0].balance, // Pay the outstanding balance
currency: 'KES',
reference,
mobile_money: {
phone: invoice[0].phone,
provider: 'mpesa',
},
metadata: {
invoice_id: invoice[0].id,
unit_number: invoice[0].unit_number,
property_name: invoice[0].property_name,
month: invoice[0].month,
tenant_name: `${invoice[0].first_name} ${invoice[0].last_name}`,
custom_fields: [
{ display_name: 'Property', variable_name: 'property', value: invoice[0].property_name },
{ display_name: 'Unit', variable_name: 'unit', value: invoice[0].unit_number },
{ display_name: 'Month', variable_name: 'month', value: invoice[0].month },
],
},
};
// Add split if agent exists
if (invoice[0].paystack_subaccount_code) {
chargePayload.subaccount = invoice[0].paystack_subaccount_code;
chargePayload.share = {
type: 'percentage',
value: invoice[0].agent_commission_percent,
};
// The agent gets their percentage, the rest goes to the main Paystack account (landlord)
}
const response = await fetch('https://api.paystack.co/charge', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYSTACK_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(chargePayload),
});
const data = await response.json();
await db.insert('payments', {
reference,
invoice_id: invoiceId,
amount: invoice[0].balance,
status: 'pending',
});
res.json({
status: 'pending',
reference,
amount: invoice[0].balance,
message: 'Check your phone for the M-Pesa prompt',
});
});
With Splits, the money flows directly to the right people. The agent does not touch the landlord's money. The landlord does not have to chase the agent for the balance. Paystack handles the math and the settlement. This alone is enough reason for many property agents to adopt a platform.
Note: Paystack Splits settle the subaccount (agent) portion separately. The main account (which can be the landlord's or the platform's) receives the remainder. If the platform charges its own fee, you can structure it as a multi-way split or deduct the platform fee from the main account balance before disbursing to the landlord.
Automatic Payment Reminders
Payment reminders are the most impactful feature in a rent collection platform. They reduce late payments without anyone having to make a phone call.
// Reminder schedule (configurable per property)
const defaultReminderSchedule = [
{ daysBefore: 5, message: 'upcoming' }, // 5 days before due date
{ daysBefore: 1, message: 'tomorrow' }, // 1 day before
{ daysAfter: 0, message: 'today' }, // On due date
{ daysAfter: 3, message: 'overdue' }, // 3 days after
{ daysAfter: 7, message: 'overdue_final' }, // 7 days after (final notice)
];
async function sendRentReminders() {
const today = new Date();
const unpaidInvoices = await db.query(
`SELECT ri.*, u.unit_number, t.phone, t.first_name,
p.name as property_name
FROM rent_invoices ri
JOIN units u ON ri.unit_id = u.id
JOIN tenants t ON ri.tenant_id = t.id
JOIN properties p ON u.property_id = p.id
WHERE ri.status IN ('unpaid', 'partial', 'overdue')`
);
for (const invoice of unpaidInvoices) {
const dueDate = new Date(invoice.due_date);
const daysDiff = Math.floor(
(dueDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)
);
const balanceKES = (invoice.balance / 100).toLocaleString();
let message = null;
if (daysDiff === 5) {
message = `Hi ${invoice.first_name}, your rent of KES ${balanceKES} for ${invoice.unit_number} is due on ${dueDate.toLocaleDateString('en-KE')}. Pay via the portal to avoid late penalties.`;
} else if (daysDiff === 1) {
message = `Reminder: Rent of KES ${balanceKES} for ${invoice.unit_number} is due tomorrow. Pay via M-Pesa through the portal.`;
} else if (daysDiff === 0) {
message = `Rent of KES ${balanceKES} for ${invoice.unit_number} is due today. Pay now to avoid late penalties.`;
} else if (daysDiff === -3) {
message = `Your rent of KES ${balanceKES} for ${invoice.unit_number} is 3 days overdue. Late penalties may apply. Pay now.`;
} else if (daysDiff === -7) {
message = `FINAL NOTICE: Rent of KES ${balanceKES} for ${invoice.unit_number} is 7 days overdue. Please pay immediately or contact the office.`;
}
if (message) {
await sendSMS(invoice.phone, message);
await db.insert('reminder_logs', {
invoice_id: invoice.id,
type: 'rent_reminder',
sent_at: new Date(),
});
}
}
}
Reminders work because they remove the social awkwardness. Instead of the caretaker knocking on doors asking for rent, a system sends a polite SMS. The tenant pays without a confrontation. The caretaker is not the bad guy. Everyone wins.
For the final notice (7 days overdue), some platforms send the message to both the tenant and the landlord/agent, so the landlord knows the situation before deciding on next steps.
Disbursement to Landlord Accounts
After rent is collected and the agent commission is split off, the landlord's share needs to reach their account. Paystack handles this in two ways depending on your setup:
- Automatic settlement. If the landlord is the Paystack account owner (or a split recipient), their share settles automatically to their registered bank account on Paystack's settlement schedule.
- Manual transfer. If the platform collects all rent into its own Paystack account and then disburses to landlords, use Paystack Transfers.
// Monthly disbursement to landlords
async function disburseLandlordFunds(month) {
// Get all properties with their collection summary
const collections = await db.query(
`SELECT
p.id as property_id,
p.name as property_name,
l.id as landlord_id,
l.name as landlord_name,
l.paystack_recipient_code,
l.preferred_settlement,
p.agent_commission_percent,
SUM(ri.paid_amount) as total_collected,
COUNT(ri.id) as invoices_count
FROM rent_invoices ri
JOIN units u ON ri.unit_id = u.id
JOIN properties p ON u.property_id = p.id
JOIN landlords l ON p.landlord_id = l.id
WHERE ri.month = $1 AND ri.status = 'paid'
GROUP BY p.id, l.id`,
[month]
);
for (const collection of collections) {
// Calculate landlord's share (total - agent commission - platform fee)
const agentShare = Math.floor(
collection.total_collected * collection.agent_commission_percent / 100
);
const platformFee = Math.floor(collection.total_collected * 2 / 100); // 2% platform fee
const landlordShare = collection.total_collected - agentShare - platformFee;
// Ensure recipient exists
let recipientCode = collection.paystack_recipient_code;
if (!recipientCode) {
recipientCode = await createLandlordRecipient(collection.landlord_id);
}
// Initiate transfer
const transferRes = 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: landlordShare,
recipient: recipientCode,
reason: `Rent disbursement for ${collection.property_name} - ${month}`,
}),
});
const transfer = await transferRes.json();
// Record the disbursement
await db.insert('disbursements', {
landlord_id: collection.landlord_id,
property_id: collection.property_id,
month,
total_collected: collection.total_collected,
agent_commission: agentShare,
platform_fee: platformFee,
landlord_share: landlordShare,
transfer_code: transfer.data.transfer_code,
status: 'sent',
});
// Send SMS to landlord
const shareKES = (landlordShare / 100).toLocaleString();
const totalKES = (collection.total_collected / 100).toLocaleString();
await sendSMS(
collection.landlord_phone,
`Rent disbursement for ${collection.property_name} (${month}): Total collected KES ${totalKES}. Your share: KES ${shareKES}. Transfer initiated.`
);
}
}
The landlord statement is important. Send a monthly summary showing:
- Total rent collected across all units
- Which units paid and which did not
- Agent commission deducted
- Platform fee (if applicable)
- Net amount disbursed
- Outstanding balances from tenants
This statement replaces the monthly call between the landlord and the agent where the landlord asks "How much did we collect?" and the agent gives an approximation. The statement is exact, auditable, and generated automatically.
Vacancy Tracking and Tenant Turnover
Property managers managing ten or more buildings need vacancy data. Which units are empty? How long have they been empty? What is the monthly revenue loss?
// Move a tenant out
app.post('/api/units/:unitId/move-out', async (req, res) => {
const { unitId } = req.params;
const { moveOutDate, depositRefund } = req.body;
const unit = await db.query('SELECT * FROM units WHERE id = $1', [unitId]);
// Check for outstanding rent
const outstanding = await db.query(
`SELECT SUM(balance) as total_balance
FROM rent_invoices
WHERE unit_id = $1 AND status IN ('unpaid', 'partial', 'overdue')`,
[unitId]
);
// Update unit status
await db.query(
'UPDATE units SET status = $1, tenant_id = NULL, lease_end = $2 WHERE id = $3',
['vacant', moveOutDate, unitId]
);
// Log the vacancy
await db.insert('vacancy_log', {
unit_id: unitId,
vacant_from: moveOutDate,
previous_tenant_id: unit[0].tenant_id,
outstanding_balance: outstanding[0].total_balance || 0,
});
res.json({
unit: unit[0].unit_number,
status: 'vacant',
outstandingBalance: outstanding[0].total_balance || 0,
depositRefund,
});
});
// Vacancy report for a property
app.get('/api/properties/:propertyId/vacancies', async (req, res) => {
const { propertyId } = req.params;
const vacantUnits = await db.query(
`SELECT u.unit_number, u.unit_type, u.rent_amount,
vl.vacant_from,
EXTRACT(DAY FROM NOW() - vl.vacant_from) as days_vacant
FROM units u
LEFT JOIN vacancy_log vl ON u.id = vl.unit_id AND vl.filled_on IS NULL
WHERE u.property_id = $1 AND u.status = 'vacant'
ORDER BY vl.vacant_from ASC`,
[propertyId]
);
const monthlyRevenueLoss = vacantUnits.reduce((sum, u) => sum + u.rent_amount, 0);
res.json({
vacantUnits,
totalVacant: vacantUnits.length,
monthlyRevenueLoss,
});
});
The vacancy report is a powerful tool for property agents when talking to landlords. "Your building has 3 vacant units, losing KES 45,000 per month" is a concrete data point that drives decisions about pricing, marketing, or property improvements.
Rent Receipt Generation
Every rent payment generates a receipt. The receipt protects the tenant (proof of payment) and the landlord (auditable record).
async function generateRentReceipt(paymentData) {
const { reference, invoiceId, amount } = paymentData;
const invoice = await db.query(
`SELECT ri.*, u.unit_number, u.rent_amount,
t.first_name, t.last_name, t.phone,
p.name as property_name, p.location
FROM rent_invoices ri
JOIN units u ON ri.unit_id = u.id
JOIN tenants t ON ri.tenant_id = t.id
JOIN properties p ON u.property_id = p.id
WHERE ri.id = $1`,
[invoiceId]
);
const receipt = {
receiptNumber: reference,
date: new Date().toISOString(),
// Tenant details
tenantName: `${invoice[0].first_name} ${invoice[0].last_name}`,
// Property details
propertyName: invoice[0].property_name,
propertyLocation: invoice[0].location,
unitNumber: invoice[0].unit_number,
// Payment details
month: invoice[0].month,
rentAmount: invoice[0].rent_amount,
penaltyAmount: invoice[0].penalty_amount,
amountPaid: amount,
previousPayments: invoice[0].paid_amount - amount,
remainingBalance: invoice[0].balance,
paymentStatus: invoice[0].balance <= 0 ? 'FULLY PAID' : 'PARTIAL PAYMENT',
};
return await createReceiptPDF(receipt);
}
Send the receipt link via SMS immediately after payment confirmation. The tenant should be able to download any past receipt from the portal at any time. When a landlord says "You did not pay March," the tenant pulls up the receipt in ten seconds.
Learn to Build Property Tech
A rent collection platform is a real product in a large market. Kenya has millions of rental properties, and the vast majority still use the notebook-and-phone-call system. Building a working alternative is a meaningful engineering project and a viable business.
The McTaba M-Pesa Integration course (KES 9,999) teaches the payment collection, splits, and transfer patterns at the core of this platform. The full fee applies as credit toward the 26-week bootcamp.
The 26-week Full-Stack Software and AI Engineering bootcamp (KES 120,000) covers the complete stack: databases, APIs, scheduling, authentication, deployment, and production-grade engineering.
Key Takeaways
- ✓Rent collection in Kenya is dominated by M-Pesa. Most tenants pay rent via M-Pesa, so your platform must make the M-Pesa experience seamless. STK push on the due date, SMS receipts, and simple balance views.
- ✓Paystack Splits handle the landlord-agent commission split automatically. When a tenant pays rent, the platform divides the payment: the landlord gets their share and the agent gets the commission, all in one transaction.
- ✓Late payment penalties must be configurable per property. Some landlords charge a flat penalty after the 5th. Others charge daily. Others have no penalty at all. Your system needs to support all these models.
- ✓Automated reminders are the single feature that most reduces collection friction. An SMS on the 25th of the month reminding the tenant that rent is due on the 1st saves everyone time.
- ✓Vacancy tracking matters for property managers handling multiple buildings. When a tenant moves out, the unit becomes vacant, rent stops being billed, and the unit appears in the vacancy report.
- ✓Landlord disbursements go to bank accounts or M-Pesa wallets via Paystack Transfers. The landlord gets a monthly statement showing rent collected, agent commission deducted, and net amount disbursed.
- ✓Receipt generation protects both parties. The tenant gets proof of payment. The landlord gets an auditable collection record.
Frequently Asked Questions
- How does Paystack Splits work for rent collection?
- Paystack Splits lets you divide a single payment between multiple recipients. For rent, the tenant pays once. Paystack automatically sends the agent's commission percentage to the agent's subaccount and the remainder to the main account (landlord or platform). The split is configured per transaction, so different properties can have different commission rates.
- Can tenants pay rent via M-Pesa?
- Yes. The system uses the Paystack Charge API with mobile_money provider set to "mpesa." When the tenant initiates a payment through the portal or responds to a payment link, an STK push is sent to their phone. They enter their M-Pesa PIN and the payment is confirmed. An SMS receipt follows immediately.
- How are late payment penalties calculated?
- The system supports three penalty models configured per property: no penalty, flat penalty (a one-time amount charged after the grace period), and daily penalty (a fixed amount per day past the grace period). Penalties are calculated automatically by a scheduled job and added to the invoice total. The tenant receives an SMS when a penalty is applied.
- How does the landlord receive their money?
- Through Paystack Transfers. If using Splits, the landlord's share settles to their registered bank account on Paystack's settlement schedule. If the platform collects all rent and disburses separately, a monthly transfer is initiated from the platform's Paystack balance to the landlord's bank account or M-Pesa wallet. The landlord receives a monthly statement showing total collections, commissions, and net disbursement.
- Can the system handle partial rent payments?
- Yes. The system tracks partial payments against the monthly invoice. If a tenant pays KES 8,000 of a KES 15,000 rent, the invoice shows a balance of KES 7,000 with status "partial." The tenant can make additional payments until the balance is zero. Each payment generates its own receipt. Note that Paystack Splits on partial payments will split whatever amount is paid.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon