Skip to content
Fintech9 min read

What building payments in Zimbabwe teaches you that Stripe never will

Multi-currency is not a formatting problem. Reconciliation is not an afterthought. And the callback you are relying on may arrive twice, late, or never.

Simbarashe Maunga
Founder & Lead Engineer

If your entire experience of taking money online is a well-documented processor with a sandbox, idempotency keys, signed webhooks and a status page, you have been working with the training wheels on. That is not an insult. Those platforms are superb, and they represent a genuine amount of hidden engineering. But they hide it so well that a generation of developers has never had to build the machinery underneath.

Building payment flows for the Zimbabwean market takes the wheels off. Here is what we have learned, most of it the expensive way.

Currency is not a display concern

The instinct is to store an amount and a currency code and format at the edge. That instinct falls apart immediately in a dual-currency economy.

An invoice raised in USD, partially settled in local currency at a rate agreed on a Tuesday, with the remainder outstanding, is not one number with a label on it. You need to record, for every movement of money:

  • the amount and currency as transacted, exactly as the customer experienced it;
  • the rate applied, and the timestamp and source of that rate;
  • the resulting amount in your reporting currency.

All three, immutably, on the payment record itself. Not derived at read time from a rates table, because that table will be updated and your historical reports will silently change underneath you. We have audited books where last quarter's revenue moved because someone corrected a rate. That is not an accounting inconvenience, it is a loss of trust in the entire system.

And store money as integer minor units. Every time. There is no version of float arithmetic on currency that ends well, and the bug does not appear in testing. It appears in a reconciliation report eight months in, off by four cents, and costs a day to find.

The callback is a hint, not a fact

Mobile money and local gateway integrations will send you a callback when a transaction completes. Sometimes. It may also:

  • arrive twice, or five times;
  • arrive before your own database transaction has committed the record it refers to;
  • arrive forty minutes late, after the customer has already retried;
  • never arrive at all, while the transaction has in fact succeeded and the customer has the SMS to prove it.

So the callback cannot be your source of truth. It can only be a prompt to go and check.

The shape that works:

  1. Create a local transaction record with your own reference before you contact the gateway at all. If the process dies mid-flight you still know what you attempted.
  2. Treat the callback as "something may have changed about reference X". Handle it idempotently: receiving it five times must produce exactly the same end state as receiving it once.
  3. On receipt, query the gateway for the authoritative status. Do not trust the payload's status field.
  4. Run a reconciliation job independently of all of this, sweeping pending transactions older than a threshold and asking the gateway what actually happened. This job is not a safety net you hope never fires. It is a primary code path, and on any given week it will be the thing that catches real money.

That fourth step is the one teams skip because everything works in testing. It is also the one that saves you, and it should be tested as carefully as the happy path.

Failure states are a product surface

The interesting difference between a payment flow that people trust and one they abandon is almost entirely in how it handles the unhappy path.

A customer whose payment is pending needs to be told it is pending, what that means, roughly how long it should take, and what happens if it does not resolve. A customer whose payment failed needs to know whether they were charged. "Something went wrong" is not an answer; it is an invitation to phone you, and then to try again and possibly pay twice.

We now write the state machine (initiated, pending, succeeded, failed, expired, reversed) and the customer-facing copy for every state, before writing the integration. It reliably surfaces states the gateway documentation never mentions, and it turns a vague integration into a specification.

Build the ledger before you need it

The strongest advice we can give: keep an append-only record of every money movement from day one, even when the product is small enough that it feels like overkill.

Not a balance column that gets updated. Entries that get appended, where the balance is derived. The moment you have a dispute, a refund, a partial settlement, a reversal, or an auditor, the mutable balance column becomes unanswerable: it can tell you what the balance is, but never how it got there. The ledger answers both, and retrofitting one onto a year of production data is a project nobody enjoys.

The part that transfers everywhere

None of this is really about Zimbabwe. Every one of these lessons (idempotency, reconciliation as a first-class job, immutable rate capture, an append-only ledger, designed failure states) is exactly what the big processors are doing internally on your behalf.

Working in a market where you have to build it yourself does not leave you behind. It leaves you with a much clearer picture of what you are actually relying on everywhere else.

paymentsfintechzimbabweintegration
Let's talk

Got a problem like this one?

We'd rather look at your actual system than guess. Tell us what you're dealing with.