Websites11 min read

Mobile Money disbursement & bulk payouts Africa: Wave + OM + MTN MoMo (2026)

Mohamed Bah·Fondateur, Kolonell
May 19, 2026
Share:
Mobile Money disbursement & bulk payouts Africa: Wave + OM + MTN MoMo (2026)

Mobile Money disbursement & bulk payouts Africa: Wave + OM + MTN MoMo (2026)

Websites

Beyond payroll, many platforms need to bulk-disburse payments to wallets: affiliate commissions, refunds, marketplace payouts, student grants, social aid. In 2026, the African B2B mobile money ecosystem is still fragmented — here's how to unify it.

TL;DR

- 4 major providers UEMOA + Anglophone: Wave, OM, MTN MoMo, Airtel Money.

- Unified architecture: payment-router with per-provider connector.

- Critical idempotency + retry + accounting reconciliation.

Unified architecture

`

[Platform]

[Bulk payout request]

[PayoutRouter — picks provider per receiver]

┌───┴───┬──────┬──────┐

↓ ↓ ↓ ↓

[Wave] [OM] [MTN] [Airtel]

↓ ↓ ↓ ↓

└───┬───┴──────┴──────┘

[Webhook callbacks]

[Reconciliation + audit log]

`

Step 1 — common provider abstraction

`ts

export interface PayoutProvider {

name: string;

countries: string[];

initiateBulk(payouts: PayoutRequest[]): Promise;

getStatus(externalId: string): Promise;

verifyWebhook(body: string, signature: string): boolean;

}

interface PayoutRequest {

externalId: string;

amount: number;

currency: 'XOF' | 'XAF' | 'GHS' | 'NGN' | 'KES';

receiverMsisdn: string;

narration: string;

metadata?: Record;

}

`

Step 2 — per-provider connector

Wave Connector

`ts

class WaveConnector implements PayoutProvider {

name = 'wave';

countries = ['SN', 'CI', 'ML', 'BF'];

async initiateBulk(payouts: PayoutRequest[]) {

// ... see Wave Business B2B article

}

async getStatus(externalId: string) {

const res = await fetch(https://api.wave.com/v1/business/disbursements/${externalId}, {

headers: { 'Authorization': Bearer ${process.env.WAVE_KEY} },

});

return res.json();

}

}

`

MTN MoMo Connector

`ts

class MtnMomoConnector implements PayoutProvider {

name = 'mtn-momo';

countries = ['CI', 'GH', 'NG', 'CM', 'UG', 'RW', 'ZM'];

async initiateBulk(payouts: PayoutRequest[]) {

const token = await this.getOAuthToken();

const results = await Promise.all(

payouts.map(p => this.singleTransfer(p, token))

);

return { batchId: crypto.randomUUID(), results };

}

private async singleTransfer(payout: PayoutRequest, token: string) {

const referenceId = crypto.randomUUID();

await fetch('https://sandbox.momodeveloper.mtn.com/disbursement/v1_0/transfer', {

method: 'POST',

headers: {

'Authorization': Bearer ${token},

'X-Reference-Id': referenceId,

'X-Target-Environment': process.env.NODE_ENV === 'production' ? 'production' : 'sandbox',

'Ocp-Apim-Subscription-Key': process.env.MTN_SUB_KEY!,

'Content-Type': 'application/json',

},

body: JSON.stringify({

amount: String(payout.amount),

currency: payout.currency,

externalId: payout.externalId,

payee: { partyIdType: 'MSISDN', partyId: payout.receiverMsisdn },

payerMessage: payout.narration,

payeeNote: payout.narration,

}),

});

return { externalId: payout.externalId, referenceId, status: 'PENDING' };

}

}

`

Smart routing

`ts

const PROVIDERS: Record = {

wave: new WaveConnector(),

mtn: new MtnMomoConnector(),

om: new OrangeMoneyConnector(),

airtel: new AirtelConnector(),

};

export async function routeBulkPayouts(payouts: PayoutRequest[]) {

const byProvider: Record = {};

for (const p of payouts) {

const provider = await detectProvider(p.receiverMsisdn);

if (!byProvider[provider]) byProvider[provider] = [];

byProvider[provider].push(p);

}

const results = await Promise.all(

Object.entries(byProvider).map(async ([providerName, providerPayouts]) => {

const provider = PROVIDERS[providerName];

const result = await provider.initiateBulk(providerPayouts);

return { provider: providerName, result };

Need a professional website?

Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.

})

);

return results.flat();

}

async function detectProvider(msisdn: string): Promise {

if (msisdn.startsWith('+221')) {

const stored = await prisma.user.findFirst({

where: { phone: msisdn },

select: { preferredWalletProvider: true },

});

return stored?.preferredWalletProvider ?? 'wave';

}

// etc. for other countries

}

`

Step 3 — reconciliation + audit

`prisma

model BulkPayoutBatch {

id String @id @default(cuid())

organizationId String

initiatedBy String

reason String

totalAmount Int

totalReceivers Int

status String

createdAt DateTime @default(now())

completedAt DateTime?

payouts Payout[]

}

model Payout {

id String @id @default(cuid())

batchId String

batch BulkPayoutBatch @relation(fields: [batchId], references: [id])

externalId String

receiverId String

receiverMsisdn String

amount Int

currency String

provider String

providerTxId String?

status String

errorMessage String?

initiatedAt DateTime @default(now())

completedAt DateTime?

@@unique([externalId, provider])

@@index([status, createdAt])

}

model PayoutAuditLog {

id String @id @default(cuid())

payoutId String

action String

payload Json

timestamp DateTime @default(now())

}

`

Step 4 — retry and idempotency

`ts

export async function retryFailedPayouts() {

const failed = await prisma.payout.findMany({

where: {

status: 'FAILED',

errorMessage: { in: ['NETWORK_ERROR', 'TEMPORARY_FAILURE'] },

initiatedAt: { gt: new Date(Date.now() - 24 * 60 * 60 * 1000) },

},

});

for (const payout of failed) {

const provider = PROVIDERS[payout.provider];

try {

const result = await provider.initiateBulk([{

externalId: payout.externalId,

amount: payout.amount,

currency: payout.currency as any,

receiverMsisdn: payout.receiverMsisdn,

narration: 'retry',

}]);

await prisma.payout.update({

where: { id: payout.id },

data: {

status: result.results[0].status === 'success' ? 'SUCCESS' : 'FAILED',

providerTxId: result.results[0].transaction_id,

},

});

} catch (e) {

// Log

}

}

}

`

Typical use cases

Marketplace commissions

Platform with 2,000 vendors paid monthly = Wave/OM/MTN bulk disbursement per their country.

Affiliates / referrers

Kolonell referrer program: monthly commissions to referrers' Wave/OM wallets.

Customer refunds

E-commerce: refund to original wallet. See Wave/OM refund →.

Grants / social aid

NGO distributing aid: 5,000 monthly recipients × 25K XOF = 125M XOF disbursed in 1h vs 3 months manual.

Agricultural subsidies

SN government program input distribution: 8,000 farmers, direct OM wallet payment = transparency + traceability.

Cost comparison

ProviderBulk feesDelaySandbox
Wave SN/CI0.5-1.0%<5 min
Orange Money1.5-2.0%5-15 min
MTN MoMo0.5-1.5%5-30 min
Airtel Money1.0-2.0%5-15 min
Bank bulk transfer200 XOF/txT+1 days

For >100 transactions: mobile money dominates on cost and delay.

FAQ

Q: Wave bulk cap?

A: 50M XOF per batch. Beyond, split into multiple batches.

Q: Disburse XOF from a USD platform?

A: Stripe Connect can pay USD to EUR/USD account, then Wise XOF conversion. Not direct to Wave wallets.

Q: Compliance?

A: SN Law 2008-12 on wallet number data processing. Strict AML/KYC if volume >50M XOF/month (BCEAO declaration).

Conclusion

African mobile money bulk payouts in 2026 = critical brick for scaling platforms. 4-12 weeks unified architecture per ambition. Massive savings vs manual payroll / bank transfers / cash agencies.

Tags:#Mobile Money#Bulk Payouts#Wave#MTN MoMo#Disbursement#Africa
Share:

Mohamed Bah

Fondateur, Kolonell

Passionate about digital and entrepreneurship in Africa, Mohamed has been helping Sénégalese businesses with their digital transformation since 2020. Founder of Kolonell, he believes every SME deserves a professional and accessible online présence.