← All notes
Payment systems Note 001 5 min read

Why a successful M-Pesa callback is not the end of a payment

In rent collection, initiating an STK Push is only the visible beginning. The difficult part is turning an asynchronous result into one correct, secure, and visible payment record.

Rent collection is rarely broken because someone cannot send money. It breaks when the result is difficult to connect to the right tenant, property, and balance.

That was the operational problem behind Kodara. Kenyan landlords were working across WhatsApp conversations, spreadsheets, manual rent chasing, and disconnected maintenance follow-up. Adding an M-Pesa button would not solve that fragmentation. Reconciliation had to be part of the product’s core architecture.

01 / State

Success is an event, not the finish line.

A successful callback from a payment provider reports an important event. It does not answer every question the product must resolve:

  • Which tenancy owns this payment?
  • Has this result already been processed?
  • Can only the correct landlord and tenant see it?
  • What happens when an automatic match cannot be made?

Until those questions have answers, the provider may be finished while the product is not. For Kodara, the callback is therefore an input to a payment workflow—not the final state of that workflow.

02 / Ownership

Give the payment a clear place to belong.

Kodara follows a strict ownership model:

Landlord Property Unit Tenancy

That hierarchy gives every rent payment a business context. A transaction is not merely an amount and a provider reference; it must belong to the correct tenancy inside the correct landlord account.

The same model supports account isolation. PostgreSQL Row Level Security enforces the boundary at the data layer, so payment visibility does not depend only on checks in an interface or application route.

03 / Processing

Protect the callback path.

Kodara places M-Pesa operations behind Supabase Edge Functions. Its callback handling is built around two guarantees: atomic processing and idempotency.

Atomic processing keeps related payment changes together. Idempotency ensures that receiving the same external result more than once cannot create the same payment more than once. Together, they turn an asynchronous provider message into a controlled state transition.

  1. 01Tenant initiates STK Push
  2. 02Daraja returns a callback
  3. 03Protected Edge Function
  4. 04Idempotency and tenancy matching
  5. MatchedBalance and history update UnmatchedReconciliation queue
04 / Recovery

An unmatched payment is still a real state.

Automatic reconciliation can fail even when an incoming payment result should not be discarded. Kodara sends failed matches into a resolvable queue instead of allowing them to become lost transactions.

This creates an honest third state: the payment is known, but it has not yet been assigned successfully. Keeping that state visible makes investigation and resolution part of the workflow.

05 / Evidence

Evidence should match the claim.

Kodara’s current build treats data rules and failure paths as implementation work. The evidence is specific—and deliberately not presented as production transaction volume.

Unit tests
26
Database assertions
22
Daraja integration
Sandbox verified
06 / Takeaway

The practical definition of complete.

A dependable payment workflow is complete when the result has been processed without duplication, matched to the correct business record or placed in a visible reconciliation queue, isolated to the correct account, and reflected in the balance and history seen by the user.

That is the difference between adding payments to a product and building a payment system.