Skip to content
Joseph Gitonga
<-All writeups

Noria Pay: one payments service instead of twenty-one integrations

2026-09-22 / 4 min / payments / m-pesa / idempotency / infrastructure

Every product that took money had rebuilt the same layer: persist the transaction, host the callback, normalise the payload, reconcile the settlement. Roughly 4,800 lines of it in one codebase and 12,000 in another. This is that layer, written once.


The library was never the problem

We already had payment libraries, one for each language we work in. Each wraps the providers competently and each stops at exactly the same place.

What a payments library deliberately leaves to you is the part that takes the time: persisting the transaction, hosting the callback, normalising the payload, and reconciling the settlement. That is not integration work. That is a small stateful service, and every product had written its own version of it.

We measured it before deciding to build. Roughly 4,800 lines of that layer in Zana. Roughly 12,000 in the billing product. A variant of it in nineteen others. Twenty-one codebases carrying the same responsibility, each with its own bugs, each needing the same fix twice a year when a provider changed something.

Noria Pay is that layer, once.

What it is and is not

It moves money and reports what moved. It does not keep your ledger.

That boundary is the whole design. You post a charge with your own reference and your own metadata, you get the payer’s next action back, you get a signed webhook when it settles, and you write your own journal entry. Pay never learns what the money meant.

The temptation to absorb the ledger is constant and has to be refused every time. A payments service that also knows your invoices becomes a second place where a balance lives, and second balances are how reconciliation breaks. The product owns the meaning; Pay owns the movement.

Four providers, one contract

Collections, payouts and refunds run across four providers covering mobile money, cards and bank transfers.

Those are three different shapes of operation across four providers, and none of them agree on anything: not the callback format, not the verification method, not what a failure looks like, not whether a timeout means the money moved.

The normalisation is the product. A caller posts a charge and gets back a next action. Whether that is an STK prompt on a phone or a hosted card checkout is a provider detail, and the calling product should never have branched on it in the first place.

The parts that are not glamorous and are the entire job

Idempotency on every write. A charge carries your reference. Replaying it returns the original charge rather than creating a second one. Without this, a retry after a network blip charges a customer twice, and you find out from the customer.

Verified callbacks. Every provider calls back differently, and a callback is untrusted input until it is verified the way that provider intends. Nothing settles on an unverified callback.

A status poller, because callbacks get lost. Callbacks are best effort from every provider here. A charge that is waiting is polled until it resolves. A system that only learns about settlement from callbacks has a permanent population of transactions stuck in limbo.

Sandbox and live kept apart. Each project brings its own provider accounts, and test traffic can never reach a live account by accident, so a misconfigured run fails rather than spending real money.

Statement reconciliation. Post a statement and it settles what it matches, which is the same problem Zana solves for invoices, solved here for raw movements.

Status

Pay is new. It was scaffolded in September 2026 and is being adopted product by product rather than all at once, starting with the flows that were already the most duplicated.

I am writing it up now because the decision it embodies is the interesting part, and that decision was made before a line was written: measure the duplication first, and only build the shared service if the number is embarrassing. 4,800 and 12,000 lines across twenty-one codebases is embarrassing.

If you have the same layer copied across several products and you are deciding whether to extract it, send me a brief. The measurement is usually worth doing before the extraction.


Read next