Moving money is easy to describe.
A customer sends money. A payment provider processes it. The recipient gets paid.
Then you add a second country, a second currency, and a second provider. The description stays the same. The engineering does not.
The problem goes beyond payments
Imagine you are building a fintech in Kenya.
You integrate M-Pesa and a payment provider. Deposits work. Customers can pay merchants and withdraw funds. Everything looks solved.
Then the business grows.
You expand into Uganda. You add another provider. Soon you have customers in Nigeria, merchants in three markets, and money moving between currencies and settlement systems that were never designed to talk to each other.
The questions change:
- How much money is actually available across all providers right now?
- Which transactions settled, and which are still in flight?
- Does our ledger match the balance each provider reports?
- What did foreign exchange cost us this month?
- Can we trace one payment from initiation to final settlement?
Those sound like finance questions. Most of them are infrastructure questions, and the answers live in your database, not in a spreadsheet.
How the money actually moves
A single deposit already passes through more states than most dashboards show.
CUSTOMER PROVIDER / MOBILE MONEY YOUR PLATFORM
| | |
| sends KES 1,000 | |
|--------------------------->| |
| | webhook: SUCCESS |
| |------------------------->|
| | ledger entry written
| | status: pending
| | |
| | settlement file, T+1 |
| |------------------------->|
| | reconciled against statement
| | status: settled
| | balance: available
| | |
| | payout request |
| |<-------------------------|
| | payout callback |
| |------------------------->|
| | entry closed
The customer sees one step. Your system sees five, and each one can fail in a different way.
1. Every provider has its own reality
Different providers expose different APIs, status vocabularies, settlement schedules, and reporting formats.
One reports a transaction as successful before the funds can be withdrawn. Another processes payouts asynchronously. A third sends settlement reports hours later, sometimes in a CSV with a different date format for every market.
Your system has to absorb those differences without losing track of the underlying transaction.
| Provider says | What it means for your ledger |
|---|---|
SUCCESS |
The customer was debited. The money may still be sitting in the provider's float. |
PENDING |
Do not credit the available balance yet. Keep polling or wait for the callback. |
SETTLED |
Funds are in the provider account and can be paid out. |
REVERSED |
Money came back, sometimes days later, sometimes with no webhook at all. |
This needs more than API clients. It needs an internal representation of financial events that stays true regardless of which provider produced them.
Practically, that means:
- stable internal references that survive provider ID changes,
- payment status kept separate from settlement status,
- idempotent handling of duplicate webhooks and retries,
- an append only record of everything that changed a balance.
A retry must not create a second payment. A webhook must be verified before it is trusted. A late event must not rewrite a month that finance already closed.
None of this matters in a demo. All of it matters the first week real money is involved.
2. Your ledger has to tell the truth
A dashboard can show a balance. That does not make the balance correct.
Suppose your ledger says KSh 500,000 and the provider reports KSh 485,000.
Where did the difference go? Pending settlements, transaction fees, refunds, failed payouts, timing, or a genuine break. Without reconciliation, that difference becomes a number somebody has to chase by hand in three dashboards and a spreadsheet.
A sound ledger keeps immutable entries, stores money in integer minor units instead of floats, and reconciles against provider statements on a schedule instead of when someone notices.
It also refuses to collapse every balance into one number. We track four:
- Available: can be paid out right now.
- Pending: collected from the customer but not settled by the provider.
- Reserved: held for a payout or refund that has not happened yet.
- In transit: sent to a provider, confirmation not received.
type Balance = {
currency: string;
available: number; // minor units, never floats
pending: number;
reserved: number;
inTransit: number;
};
// The dashboard shows one number. This is the explanation behind it.
const kes: Balance = {
currency: "KES",
available: 412_000_00,
pending: 58_000_00,
reserved: 15_000_00,
inTransit: 15_000_00,
};
Add those four and you get the KSh 500,000. Take the pending and in transit money out and you get the KSh 470,000 that is genuinely spendable. Neither number is the truth on its own. The breakdown is.
Reconciliation is what keeps that breakdown honest:
provider A --> webhooks --+
provider B --> status polling --+--> transaction state --+
provider C --> settlement file --+ |
v
+---------------------+
| internal ledger |
+---------------------+
|
provider statements --------------------------------------> |
v
+---------------------+
| match engine |
+---------------------+
| | |
matched timing break
close wait investigate
Three outcomes, and only one of them is a bug. Timing differences resolve themselves. Real breaks need a person, an audit trail, and enough context to answer "what happened to this money" without opening five tabs.
3. Cross border adds a second set of numbers
Within one currency, a payment has one amount. Across borders, it has several.
A customer pays in Kenyan shillings and the recipient is paid in naira. Between those two events sit an exchange rate, a spread, provider fees, and time. The rate you quoted is not always the rate you got.
KES 10,000 collected
|
| collection fee 1.5% -150 KES
v
KES 9,850 converted
|
| quoted rate 11.90 -> 117,215 NGN
| executed rate 11.72 (rate moved) -> 115,442 NGN
v
NGN 115,442
|
| payout fee -1,200 NGN
v
NGN 114,242 delivered
The transaction succeeded. The recipient got paid. And the business still lost money it never budgeted for, because the spread moved while the payment was in flight.
That is why the system has to track the quoted rate, the executed rate, the fees actually charged, and the final settlement amount as separate facts. Merge them into one "amount sent" field and you can no longer explain your own margin.
At scale, exposure matters as much as volume. A business moving money across four currencies needs to know what it is holding, what it is exposed to, and what each corridor actually costs.
4. Observability means watching the money, not just the service
In a normal application, a healthy dashboard means the API is up and latency is fine.
In financial infrastructure, that is the least interesting signal. You also need to know whether money is moving correctly.
The signals that earn their place on the wall:
- Unmatched transactions and the size of each reconciliation break.
- Payment success and failure rates, split by provider and corridor.
- Settlement delays and payouts that have not confirmed.
- Fees and conversion costs by currency.
- Sudden changes in available liquidity.
These are the numbers that turn a vague "something feels off" into a specific investigation.
A payment API returning HTTP 200 means the request worked. It says nothing about whether the money arrived.
5. The real work is connecting it
Payment integrations, ledgers, reconciliation, FX records, and treasury reporting are usually built as separate systems by separate teams.
They describe the same financial operation from different angles, and the value appears when they agree.
That means building for questions, not just screens:
- Can every balance be traced back to the entries that produced it?
- Can every entry be traced back to a provider event?
- Can a break be investigated without leaving the system?
- Can an operator answer "where is this money" in one place?
This matters most for early stage fintechs. They need dependable financial operations long before they can afford a treasury team to assemble the picture by hand.
What we are building at FinoQ
At FinoQ, we are building financial infrastructure that gives fintech businesses visibility into their money as they scale across Africa.
The goal is to connect provider liquidity, internal records, reconciliation, FX exposure, and fees so a business can see its financial position in one place, and explain it.
Because moving money is only one part of a reliable financial product. You also need to know where the money is, what happened to it, what it cost, and whether your records match reality.
Final thoughts
Africa's fintech opportunity is not only about faster payments or more customers. It is also about the infrastructure that keeps increasingly complex operations honest as companies expand across borders.
Better rails got us here. Better financial visibility is what carries the next stage.
That is an engineering problem, and a good one to work on.
This post started as a conversation on LinkedIn. FinoQ also writes about fintech treasury in Africa.
Further reading
- Designing a cross-border payment system by Weston Carnes
- Why your order system and your payment provider disagree by 0xenx
- Reconciliation in payment systems by Rahman Nugar
- We built a Stripe for African mobile money, then discovered why it could not exist by adodanieln
Top comments (7)
The SUCCESS versus SETTLED split is the detail that sticks with me, money debited but still sitting in provider float changes how a balance should read.
Exactly! A successful transaction doesn't always mean the funds are settled and available. Keeping those states separate is essential for accurate balances and reliable reconciliation, especially when working with multiple providers.
Multiple providers is the part that makes it harder. Each one can settle on its own clock, so the balance has to wait for all of them.
Exactly. Once multiple providers are involved, settlement becomes a coordination problem, not just a payment status problem. Each provider has its own timelines, so the ledger needs to track what is confirmed, what is still in transit, and what is actually available to spend.
That ledger split is the part that makes it workable in practice. Confirmed, in transit, and available need separate columns, otherwise a payment looks spendable before it actually is.
the four-balance split is the part most teams skip, then discover the hard way during their first reconciliation break. collapsing everything into one dashboard number hides exactly the money you need to explain when a provider says SUCCESS but the float hasn't released. the 'in transit' bucket deserves its own alerting too: payouts sitting there past their expected confirmation window are usually the first sign a corridor is degrading, well before any reconciliation break shows up.
Well put. The in-transit bucket is particularly important because it gives teams visibility into delays before they turn into reconciliation problems. It's not enough to know that a transaction succeeded; you need to know where the funds are, whether they've settled, and how long they've been waiting. That's the kind of visibility fintech infrastructure needs.