RESOLV
← Insights

Financial services

Integrating mobile money without losing a transaction

Mobile money integrations fail quietly: a timeout here, a duplicated callback there, and the ledger no longer matches the provider. This guide covers the engineering patterns that keep every transaction accounted for.

· 5 min read

Across East Africa, mobile money is how many customers pay fees, repay loans, receive salaries and settle bills. For a bank, microfinance institution, university or utility, integrating with a mobile money provider is therefore core infrastructure. Yet these integrations are frequently built as if the network were reliable and every message arrived exactly once. Neither is true. Requests time out after the provider has acted on them, callbacks arrive late, twice or not at all, and the customer sees a debit on their phone that the institution's system does not know about. The objective is simple to state: every shilling or dollar that moves must be recorded once, correctly, and be reconcilable against the provider's records. Meeting it requires a handful of disciplined patterns.

State, idempotency and timeouts

A payment is not a boolean. It moves through states such as initiated, submitted, pending, succeeded, failed and reversed, and each transition should be explicit, timestamped and permanent. Store the payment record before calling the provider, not after, so that a crash mid-request still leaves a trace. Never delete or overwrite a state; append transitions. This makes it possible to answer, at any moment, exactly what the system believed about a transaction and why. When a request times out, the safe instinct is to retry. Without idempotency, a retry can charge a customer twice or disburse a loan twice. Generate a unique idempotency key for each logical payment, store it with the payment record, and send it with every attempt. If the provider supports idempotency keys, it will recognise the retry and return the original result. If it does not, use your own unique reference in the provider's reference field and query the transaction status by that reference before retrying.

  • One key per logical payment, never per attempt.
  • Enforce uniqueness in the database with a constraint, not only in application code.
  • Make your own inbound endpoints idempotent too, keyed on the provider's transaction identifier.
  • Retry with backoff and a ceiling, then hand over to a status query rather than retrying indefinitely.

The most common cause of lost or duplicated transactions is treating a timeout as a failure. A timeout means the outcome is unknown. Mark the payment as pending, tell the customer that confirmation is in progress, and resolve the state by querying the provider or waiting for the callback. Only mark a payment failed when the provider says so or when the reconciliation process confirms it never happened. Set a maximum age for pending payments after which they are escalated to an operations queue with an owner.

A timeout is not a failure; it is a question the system has not yet answered.

Verify every webhook

Callbacks that confirm payments are an obvious target for fraud: anyone who can post a convincing message to the callback URL can mark an unpaid invoice as paid. Verify each callback before acting on it.

  • Validate the signature using the provider's documented scheme and a secret stored in a secrets manager, comparing in constant time.
  • Reject callbacks with stale timestamps to limit replay, and record processed callback identifiers to ignore duplicates.
  • Where signatures are not offered, restrict the endpoint to the provider's published addresses and confirm the transaction by querying the provider's API before crediting.
  • Check that the amount, currency and reference in the callback match the payment you created.
  • Acknowledge quickly and process asynchronously, so slow processing does not trigger provider retries.

Prevent double charges, then reconcile daily

Many duplicate payments originate with the customer: a slow screen, a second tap, a resubmitted form. Disable submission once a payment starts, show a clear pending state, and when a customer initiates a payment for an invoice that already has one pending, show the existing payment rather than creating another. On the server, refuse to create a second active payment for the same obligation unless the first has reached a terminal state. Real-time processing will never be perfect, so reconciliation is the control that catches what it misses. Retrieve the provider's statement or settlement report daily and match it line by line against your ledger using the provider transaction identifier and your own reference.

  • Matched: both sides agree on amount, status and date. No action.
  • In provider but not in ledger: money moved that you did not record. Investigate and credit the customer promptly.
  • In ledger but not in provider: you recorded a payment that did not happen. Reverse it and investigate the cause.
  • Mismatched amount or status: hold for review by an operations officer, with the evidence attached.

Automate the matching, but keep exceptions with named people and deadlines. Reconcile settlement to the bank account as well, so fees and timing differences are understood rather than absorbed. Keep an immutable record of every request and response exchanged with the provider, with sensitive fields masked, linked to the payment record. Restrict who can change payment states manually, require a reason and a second approver for adjustments, and log those actions separately. Where card data is ever involved, PCI DSS applies; for mobile money, apply the same discipline to credentials and customer data. Monitor the rate of pending, failed and unmatched transactions so that a provider incident is noticed by your team before it is reported by customers. Agree an escalation route with the provider in advance, including a named technical contact, so that an incident is not the first time the two teams speak.

Pitfalls to avoid

  • Crediting the customer on the strength of a redirect or client-side message rather than a verified server-side confirmation.
  • Storing provider credentials in source code or configuration files committed to a repository.
  • Testing only the happy path; deliberately simulate timeouts, duplicate callbacks and out-of-order messages.
  • Leaving reconciliation to the finance team's spreadsheets with no feedback into the engineering backlog.

None of these patterns is exotic. Together they turn an integration that mostly works into one that can be trusted with customers' money, and that can prove it when asked. The cost of building them in from the start is modest; the cost of retrofitting them after customers have been charged twice is not.

Tell us what can't fail.

A senior engineer reviews every enquiry and replies within one business day.