M-Pesa handles over 90% of rent payments in Kenya. But most landlords still reconcile these payments manually — downloading statements, matching transactions to tenants line by line in Excel. We automated this entire flow.
Here is how we built the M-Pesa rent collection system inside
2. IPN (Instant Payment Notification) callbacks
Safaricom sends a POST request to our endpoint every time a payment is made. This gives us real-time data — no polling, no delays.
# Simplified IPN handler
@csrf_exempt
def mpesa_callback(request):
data = json.loads(request.body)
transaction_id = data['TransID']
amount = Decimal(data['TransAmount'])
account_ref = data['BillRefNumber'] # Unit number
phone = data['MSISDN']
# Find tenant by unit reference
tenant = Tenant.objects.filter(
unit__unit_number__iexact=account_ref,
unit__property__organization=org
).first()
if tenant:
# Record payment
payment = MpesaPayment.objects.create(
tenant=tenant,
amount=amount,
transaction_id=transaction_id,
phone_number=phone
)
# Auto-reconcile against outstanding invoice
reconcile_payment(payment)
# Send SMS receipt
send_receipt_sms(tenant, amount, transaction_id)
return JsonResponse({'ResultCode': 0})
3. Fuzzy matching for account references
Tenants do not always type their unit number correctly. "A3" might come in as "a3", "A 3", "Unit A3", or "apt3". We normalize the account reference before matching:
def normalize_account_ref(ref):
"""Normalize M-Pesa account reference for matching."""
ref = ref.strip().upper()
ref = re.sub(r'[^A-Z0-9]', '', ref) # Remove special chars
ref = ref.replace('UNIT', '').replace('APT', '').replace('ROOM', '')
return ref
4. STK Push for proactive collection
Instead of waiting for tenants to initiate payment, we can trigger an STK Push — a payment prompt appears on the tenant's phone:
def trigger_stk_push(tenant, amount):
"""Send M-Pesa payment prompt to tenant's phone."""
payload = {
'BusinessShortCode': PAYBILL_NUMBER,
'Amount': amount,
'PartyA': tenant.phone_number,
'PartyB': PAYBILL_NUMBER,
'PhoneNumber': tenant.phone_number,
'AccountReference': tenant.unit.unit_number,
'TransactionDesc': f'Rent for {tenant.unit.unit_number}'
}
response = daraja_api.stk_push(payload)
return response
This is triggered automatically when reminders are sent — the tenant gets an SMS reminder AND a payment prompt simultaneously.
Results
- Payment matching: 95%+ auto-reconciled without human intervention
- Receipt delivery: under 10 seconds from payment to SMS receipt
- Late payment reduction: 40-60% reduction with automated reminders + STK push
- Admin time saved: 8+ hours/month for a 50-unit portfolio
The Stack
Backend: Django + Django REST Framework
Database: PostgreSQL
Task Queue: Celery + Redis (for async SMS sending and reconciliation)
M-Pesa Integration: Safaricom Daraja API v2
SMS: Africa's Talking API
Hosting: AWS (ECS + RDS + S3)
Frontend: React (web) + React Native (mobile)
Try It
The platform is live at
SOCIAL SHARE CARD GENERATOR