Founder & Lead Engineer, RAITHub
A rental management system for Kenya should collect rent through an M-Pesa Paybill with each unit's code as the account reference, store every payment Safaricom confirms exactly once, and match it to that unit's open charges. Partial payments reduce the balance, overpayments become credit, and anything unmatched goes to a review queue. A KES 25,000 rent paid in two instalments should reconcile with no manual step.
This is engineering guidance for PropTech founders and property managers in Kenya who are outgrowing spreadsheets or a generic tool. RAITHub is a software studio in Dhaka, Bangladesh, with no office in Kenya, and we have not shipped an M-Pesa or Daraja integration. What we have shipped is close to it: PropDesk, a property management platform with Stripe rent collection, leases, 4 roles, 5 scheduled jobs and 1,024 automated tests, and bKash, Nagad and SSLCommerz, Bangladesh's mobile-money-style wallets and gateway, in TheSkinProof, the founder's own marketplace venture. The reconciliation problem is the same one; the rail is new. Safaricom's details below come from its Daraja developer portal and its open-source SDKs, checked on 30 September 2026; confirm them against the portal for your account before building.
How do tenants in Kenya pay rent through M-Pesa?
Mostly in one of three ways, and they differ in the one thing reconciliation depends on: whether the payment carries a reference you chose. A payment with a reliable reference matches itself. A payment without one needs a person.
| Channel | What the tenant does | Reference you receive | Reconciliation effort |
|---|---|---|---|
| Paybill (C2B) | Enters your business number, then an account number, then the amount | The account number the tenant typed | Low, if the account number is the unit code and you normalise it |
| Till (Buy Goods) | Enters your till number and the amount | No account number | High: you match on phone number and amount, and guess when a tenant pays from a relative's phone |
| M-Pesa Express (STK Push) from your tenant portal | Taps "Pay rent", then enters the M-Pesa PIN in a prompt on the phone | The account reference your system sent | Lowest: your system started the payment and knows the invoice |
| Bank transfer or standing order | Pays from a bank app, often with a free-text narration | Whatever the tenant typed in the narration, if anything | Medium: fuzzy matching on narration, amount and date |
For a portfolio of 20 to 300 units, the practical design is a Paybill for tenants who pay from the M-Pesa menu, plus M-Pesa Express in the tenant portal for those who prefer a button. Both land in the same ledger. Avoid a Till for rent unless you are prepared to reconcile by hand.
What should the account reference be on a rent Paybill?
A short unit code that is printed on the lease, the invoice and the SMS reminder, and that your system can recognise however the tenant types it. The account number is free text, so tenants will enter kil b12, KILB12 or KIL-B-12 for the same flat. Design for that on day one.
- Use the unit, not the tenant. Tenants change; the unit does not. The ledger links a payment to the unit, and the unit to whoever holds the active lease on the payment date.
- Keep it short and unambiguous. A property prefix and a unit number, such as
KIL-B12. Avoid codes where 0 and O, or 1 and I, could be confused. - Normalise before matching. Uppercase and strip everything that is not a letter or digit, then look up the normalised form. Store both what the tenant typed and the normalised value.
- Make codes unique after normalisation.
KIL-B12andKILB-12must not both exist. A unique index on the normalised column enforces it.
// rent/reference.ts
export function normaliseRef(raw: string): string {
return raw.toUpperCase().replace(/[^A-Z0-9]/g, '')
}
// normaliseRef(' kil b-12 ') === 'KILB12'
A tenant who manages several units, or a company tenant, should get one code per unit. One payment for two units is possible, but it always needs a person to split it, so it belongs in the review queue rather than in automatic allocation.
How does an M-Pesa rent payment reach your system?
Through URLs you register with Safaricom, called asynchronously after the money moves. For Paybill payments, the C2B Register URL API takes a ShortCode, a ConfirmationURL, a ValidationURL and a ResponseType, which Safaricom's own SDK describes as the default for a timeout: "Mpesa will by default Complete or Cancel the transaction" (Safaricom mpesa-node-library). For M-Pesa Express, the request carries a CallBackURL and an AccountReference, and the result is posted back to that URL. Safaricom's PHP SDK puts the model plainly: "M-Pesa APIs are asynchronous", and a valid request "is added to a queue" (Safaricom mpesa-php-sdk).
Three rules follow for a rental system:
- Store the confirmation before doing anything else. Write the raw payload and the M-Pesa transaction ID into a receipts table, keyed on that ID, then acknowledge. Allocation runs afterwards, from the table.
- A repeated confirmation must change nothing. Insert with the transaction ID as the primary key and ignore conflicts. The same receipt arriving twice is then harmless.
- Do not reject rent because the reference is wrong. The validation URL lets you refuse a payment before it completes. Refusing rent because a tenant typed
B21instead ofB12fails the tenant at the worst moment. Accept it, and route it to review.
-- Every confirmation Safaricom sends, stored once, before allocation.
CREATE TABLE mpesa_receipts (
trans_id text PRIMARY KEY, -- the M-Pesa transaction ID
bill_ref_raw text NOT NULL, -- what the tenant typed
bill_ref_norm text NOT NULL, -- normaliseRef(bill_ref_raw)
unit_id bigint REFERENCES units(id), -- null until matched
amount_cents bigint NOT NULL CHECK (amount_cents > 0),
paid_at timestamptz NOT NULL,
status text NOT NULL DEFAULT 'unmatched', -- unmatched | allocated | review | refunded
payload jsonb NOT NULL -- the raw confirmation, for audits and disputes
);
-- INSERT ... ON CONFLICT (trans_id) DO NOTHING
-- makes a duplicate confirmation a no-op.
If your team has not built webhook handlers against a real provider before, the failure modes are the same as elsewhere; the guide to testing payments and webhooks covers replaying duplicates and dropped deliveries.
How do you match M-Pesa payments to units and charges?
In two steps: find the unit, then allocate the money across that unit's open charges. Keep them separate. A payment can be matched to a unit and still wait for a person to decide how to allocate it, for example when the tenant writes to say it was for the deposit.
Finding the unit, in order of confidence:
- The normalised reference equals a unit code: matched.
- The reference is close to exactly one unit code, such as two transposed characters: suggested, confirmed by staff with one click.
- The paying phone number belongs to exactly one current tenant: suggested, not automatic, because relatives and employers pay rent too.
- None of the above: unmatched, in the review queue, with the raw reference visible.
The cases below are where rent systems either reconcile quietly or generate phone calls. Each one needs a decided behaviour and a test.
| Case | Example | What the system should do |
|---|---|---|
| Exact payment | KES 25,000 against a KES 25,000 charge, reference KIL-B12 | Allocate and mark the charge paid; send a receipt SMS |
| Partial payment | KES 15,000 on the 1st, KES 10,000 on the 5th | Allocate each; the charge stays open at KES 10,000, then closes |
| Overpayment | KES 27,000 against KES 25,000 | Close the charge; hold KES 2,000 as credit and apply it to the next charge automatically |
| Arrears plus current rent | KES 40,000 when KES 15,000 is overdue and KES 25,000 is due | Allocate by the landlord's stated order, usually oldest charge first |
| Wrong or missing reference | B21, RENT or the tenant's name | Accept, store raw, send to review with suggestions |
| Duplicate confirmation | The same transaction ID arrives twice | No second allocation; log it |
| One payment for two units | KES 50,000 with reference B12 B14 | Review: a person splits it |
| Payment after the tenant moved out | Rent for a unit whose lease ended | Match to the former tenant's account, not the new one; review for refund or final balance |
| Reversal | A transaction reversed after allocation | Reverse the allocation with its own ledger entry; never delete the original |
How should partial payments and overpayments be handled?
As ledger entries against charges, never as edits to a "balance" field. Every charge (rent, water, service charge, late fee) is a row with an open amount. Every payment is allocated across open charges in a stated order, and whatever is left becomes credit. A balance is then something you compute, so it can always be explained line by line to a tenant or an auditor.
// rent/allocate.ts
// Amounts are integers in cents: KES 25,000.00 is 2500000.
export interface Charge {
id: string
dueDate: string // YYYY-MM-DD
openCents: number
}
export interface Allocation {
chargeId: string
cents: number
}
/** Oldest charge first; anything left over becomes credit on the unit's account. */
export function allocate(paymentCents: number, charges: Charge[]) {
if (!Number.isSafeInteger(paymentCents) || paymentCents <= 0) {
throw new Error('bad amount: ' + paymentCents)
}
const open = charges
.filter((c) => c.openCents > 0)
.sort((a, b) => a.dueDate.localeCompare(b.dueDate) || a.id.localeCompare(b.id))
let left = paymentCents
const allocations: Allocation[] = []
for (const c of open) {
if (left === 0) break
const cents = Math.min(left, c.openCents)
allocations.push({ chargeId: c.id, cents })
left -= cents
}
return { allocations, creditCents: left }
}
Walk the worked example through it. Rent of KES 25,000 is due on 1 March. The tenant pays KES 15,000 on the 1st: one allocation, KES 10,000 still open. They pay KES 12,000 on the 5th: KES 10,000 closes March, and KES 2,000 becomes credit. When April's charge is created, the credit is allocated to it first, so the tenant's April reminder says KES 23,000, not KES 25,000.
Three details decide whether this holds up in production:
- Allocate inside one database transaction that locks the unit's open charges, so two payments arriving in the same second cannot both close the same charge.
- Late fees check the ledger, not the calendar. A late fee applies only if the charge is still open after the grace period. The online rent collection guide covers grace periods and reminders that send exactly once, which PropDesk runs as scheduled jobs.
- The allocation order is the landlord's policy. Oldest first is common, but some landlords want rent cleared before water or late fees. Make it a setting, show it on the tenant statement, and test it.
How do you reconcile against the M-Pesa and bank statements?
With a daily job that compares three records: the confirmations your system received, the statement your Paybill account provides, and, if the Paybill pays out to a bank account, the bank statement. The transaction ID is the join key between the first two.
-- Daily: statement lines the ledger never saw, and ledger receipts missing from the statement.
SELECT s.trans_id, s.amount_cents, 'missing_in_ledger' AS problem
FROM mpesa_statement_lines s
LEFT JOIN mpesa_receipts r USING (trans_id)
WHERE r.trans_id IS NULL
UNION ALL
SELECT r.trans_id, r.amount_cents, 'missing_on_statement'
FROM mpesa_receipts r
LEFT JOIN mpesa_statement_lines s USING (trans_id)
WHERE s.trans_id IS NULL
AND r.paid_at < now() - interval '1 day';
A line that is missing_in_ledger means a confirmation never reached you: your URL was down, or a deploy dropped it. Import it from the statement as a receipt, and it flows through matching like any other. A line missing_on_statement is rarer and more serious; it should alert a person the same day.
Bank matching is different in kind. When a Paybill pays out to a bank account, the bank sees transfers of accumulated funds, not one line per tenant. Match those bank lines to payout totals on the M-Pesa side, not to tenants. Tenants who pay rent by bank transfer are the exception: match those on amount, date and whatever reference they wrote in the narration, and expect more of them to need review.
What else does a rental management system in Kenya need?
Everything a property management product needs anywhere, with local choices in a few places. The payment ledger is the hard part; the rest is well understood.
- Leases and charges: monthly rent, deposits, service charge, water billed from meter readings, and scheduled rent increases.
- Roles: landlord or owner, property manager, caretaker or agent, and tenant, each seeing only their own properties. PropDesk runs on 4 roles; the tenant portal features guide lists what tenants actually use.
- Reminders by SMS before and after the due date, generated from the ledger so a tenant who has paid never gets chased.
- Owner statements per property and month, built from the same ledger entries, so the owner's report and the tenant's statement can never disagree.
- Maintenance requests with photos and status updates.
- A tenant web app or PWA (a progressive web app, installable from the browser) rather than a native app, so tenants on any phone can pay and check their balance.
The wider build, from data model to launch, is in the property management software development guide.
What does Kenya's Data Protection Act mean for rent data?
That payment records are treated as high-risk if they leak. Kenya's Data Protection (General) Regulations, 2021 list the personal data whose breach is taken to create a real risk of harm, and the Second Schedule includes "the deposit or withdraw of monies by a data subject with any entity" and the existence and amount of any debt owed (Data Protection (General) Regulations, 2021, regulation 37 and Second Schedule). A rent ledger is exactly that: payments in, arrears owed.
In practice: restrict who can see tenant ledgers by role and property, log access, keep phone numbers and payment payloads out of analytics tools, and have a written breach procedure. The Office of the Data Protection Commissioner's FAQs describe a registration exemption for entities with annual turnover below five million shillings and fewer than ten people, with some sectors not exempt regardless (ODPC FAQs). If a team outside Kenya will access the data, the transfer rules apply too. This is general information, not legal advice; confirm the current rule with your adviser and the ODPC.
Should you buy rental software or build your own in Kenya?
Buy if an existing product reconciles your M-Pesa payments the way you collect them, and build when the reconciliation or the owner reporting is the reason you are losing time. The cost of rent collection that almost works is paid every month by staff.
| Your situation | Lean towards |
|---|---|
| Under about 20 units, one owner, tenants pay by Paybill with the right reference | An existing rental tool, or a spreadsheet with discipline |
| A tool you already use, but staff reconcile M-Pesa statements by hand every month | Ask the vendor about C2B integration first; build if they cannot |
| Several owners, service charges, water billing and agents with limited access | Custom, or a tool that models all of it; few do |
| You are a PropTech startup selling to landlords | Custom: the reconciliation engine is your product |
For a first budget, the MVP cost estimator gives a range in minutes, and the real estate industry page shows the property work RAITHub has shipped.
Why RAITHub for this?
- Rent logic has shipped. PropDesk, built by RAITHub, collects rent through Stripe, with leases, late fees after a configurable grace period, reminders, payment plans, 4 roles and 5 scheduled jobs, under 1,024 automated tests (932 unit, 92 end to end).
- Mobile-money-style rails have shipped. TheSkinProof, the founder's own venture rather than a client project, runs bKash, Nagad, SSLCommerz and cash on delivery through one checkout, with 750+ tests across 217 API endpoints. The bKash and SSLCommerz guide shows the same confirm-server-to-server and record-once patterns this post applies to M-Pesa.
- Honest scope. M-Pesa, Daraja C2B and M-Pesa Express are new work for us, quoted as such and built against Safaricom's sandbox with the reconciliation cases above as tests.
- Working hours that fit Nairobi. Nairobi is on UTC+3 and Dhaka on UTC+6, which gives about 6 shared working hours. The working week is agreed with you.
- Clear terms. A free 15-minute technical audit, then a fixed written quote. You own the code and the IP. See the API and backend development service for the ledger and integration work.
When you don't need us
- An existing rental product already reconciles your Paybill the way you collect rent. Configure it; do not rebuild it.
- You need a vendor with M-Pesa integrations already in production that you can inspect. RAITHub does not have one yet.
- You need someone on site in Nairobi, for example to train caretakers in person. RAITHub has no office in Kenya.
- You need a native Android or iOS app. RAITHub builds web apps and PWAs, not native mobile apps.
- Your question is about tenancy law, deposits or tax. Ask a Kenyan lawyer or accountant; we build the software that records their answer.
Last reviewed: 30 September 2026. Safaricom and ODPC sources checked on 30 September 2026. General information only; confirm legal points with your adviser.
If you are planning a rental system for Kenya, book the free 15-minute technical audit. Bring a month of anonymised M-Pesa statement lines and your unit list, and we will show you on the call how many would match automatically.
Frequently asked questions
How do I reconcile M-Pesa rent payments automatically?
Use a Paybill with each unit's code as the account number, register a confirmation URL with Safaricom, and store every confirmation once, keyed on the M-Pesa transaction ID. Normalise the reference, match it to a unit, allocate the amount to open charges oldest first, and send anything unmatched to a review queue. A daily job compares your receipts with the Paybill statement.
Should I use a Paybill or a Till number for rent in Kenya?
A Paybill. It asks the tenant for an account number, which can be the unit code, so payments match themselves. A Till payment carries no account reference, so you have to match on phone number and amount, which fails whenever someone else pays the rent.
What happens when a tenant pays part of the rent through M-Pesa?
The payment is allocated to the open charge and the charge stays open for the remainder. For example, KES 15,000 against KES 25,000 leaves KES 10,000 open. The next payment closes it, and any excess becomes credit applied to the next month's charge.
What if a tenant types the wrong account number on the Paybill?
Accept the payment rather than rejecting it, store what they typed, and put it in a review queue with suggested units, such as a unit code with two characters transposed or the unit linked to the paying phone number. A person confirms the match with one click.
Has RAITHub built an M-Pesa rent collection system?
No. RAITHub has built PropDesk, which collects rent through Stripe with 1,024 automated tests, and runs bKash, Nagad and SSLCommerz in TheSkinProof, the founder's own venture. M-Pesa and Daraja work would be quoted as new work, using the same ledger and reconciliation patterns.
Is rent payment data sensitive under Kenya's Data Protection Act?
Kenya's 2021 General Regulations list deposits and withdrawals of money, and debts owed, among the data whose breach is taken to create a real risk of harm. Treat a rent ledger accordingly, restrict access by role, and confirm your obligations with your adviser and the ODPC.
Related posts
Ready to discuss your project?
Book a free 15-minute technical audit with our engineering team.