DEV Community

Automating M-Pesa Rent Collection in Kenya: The Technical Architecture

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 HomeManager, our property management platform for East Africa.

The Problem

A landlord with 50 tenants receives 50+ M-Pesa transactions every month. Each transaction has:

  • Sender phone number
  • Amount
  • Account reference (if Paybill)
  • Transaction ID
  • Timestamp

To reconcile manually:

  • Download M-Pesa statement from Safaricom portal
  • Open tenant spreadsheet
  • Match each transaction to a tenant
  • Mark as paid
  • Send receipt manually
  • Identify who has not paid
  • Send reminders individually

For 50 tenants, this takes 4-8 hours monthly. For 200 tenants, it is a full-time job.

The Architecture

We use Safaricom's Daraja API (M-Pesa API) with a Django backend.

Payment Flow

Tenant opens phone โ†’ Selects Lipa na M-Pesa โ†’ Paybill โ†’ Enters business number (landlord's Paybill) โ†’ Enters account number (their unit number, e.g., "A3") โ†’ Enters amount โ†’ Confirms with PIN

Safaricom processes payment โ†’ Sends IPN callback to our server โ†’ Our server receives transaction data

Our system:
โ†’ Validates the callback (security check)
โ†’ Extracts: amount, phone, account_ref, transaction_id
โ†’ Matches account_ref to tenant unit
โ†’ Credits tenant account
โ†’ Generates receipt
โ†’ Sends SMS receipt to tenant
โ†’ Updates dashboard in real-time

Key Technical Decisions

1. Paybill over Till Number

Paybill allows an account reference field. This is critical - the tenant enters their unit number, which the system uses to auto-match the payment. Without this (Till Number), you only get a phone number and have to look up which tenant owns that number. Full comparison: Paybill vs Till for Rent

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 homemanager.buniva.co.ke. We handle M-Pesa integration setup for landlords - they do not need to deal with the Daraja API themselves.

If you are building fintech for East Africa and want to discuss M-Pesa integration patterns, happy to connect. More on property management automation in Kenya: buniva.co.ke/blog

Comments

No comments yet. Start the discussion.