Mobile money integration patterns for African fintech
Wave, Orange Money, Free Money: the API gaps, callback traps, and idempotency pitfalls that production systems expose — and how to architect around them.
Mobile money rails in West Africa are not uniform. Wave, Orange Money, and Free Money each expose different callback semantics, settlement windows, and retry behaviours. A payment marked `SUCCESS` by the provider SDK is not always a settled transaction — and building as though it is will surface as a reconciliation nightmare six months into production.
The callback problem
Most providers deliver a webhook callback asynchronously — sometimes seconds after the payment, sometimes minutes later, occasionally not at all if the endpoint was unavailable. Your architecture must decouple payment initiation from payment confirmation: store the intent immediately, update state only when a verified callback arrives, and run a reconciliation job that polls the provider status endpoint for any intents still pending after a configurable timeout.
public enum PaymentStatus {
INITIATED, // intent saved, provider call pending
PROVIDER_SENT, // request dispatched to provider API
PENDING, // provider acknowledged, awaiting callback
SUCCESS, // verified callback received
FAILED, // provider failure or timeout reconciliation
REFUNDED // post-settlement reversal
}
@Transactional
public PaymentIntent handleCallback(CallbackPayload payload) {
PaymentIntent intent = repository
.findByProviderRef(payload.getReference())
.orElseThrow(() -> new UnknownReferenceException(payload.getReference()));
// Reject replays: idempotency on callback
if (intent.getStatus() == PaymentStatus.SUCCESS
|| intent.getStatus() == PaymentStatus.FAILED) {
return intent; // already terminal — discard duplicate
}
intent.setStatus(payload.isSuccessful()
? PaymentStatus.SUCCESS : PaymentStatus.FAILED);
intent.setSettledAt(Instant.now());
auditLog.record(intent, "callback", payload.getRawBody());
return repository.save(intent);
}Reconciliation as a first-class job
Run a scheduled reconciliation job — every 5 minutes for active intents, every hour for the prior-day batch. For each intent in `PENDING` status beyond the provider's typical settlement window, query the provider status API directly. Treat divergence as an alert: if your ledger says `PENDING` but the provider says `SUCCESS`, apply the state transition and page on-call; if the provider says `FAILED`, trigger the refund workflow and notify the user before they notice.
The systems that scale to millions of transactions without breaking finance operations are not the ones with the fastest provider integrations — they are the ones where every money state is explicit, observable, and recoverable without manual intervention.
