Manual payroll in Senegal or Ivory Coast = accounting nightmare: bank transfers one by one, cash for unbanked, recurring delays, employee conflicts. Wave Business has offered since 2024 a B2B API to disburse salaries in bulk via mobile money.
TL;DR
- Wave Business B2B API: bulk disburse payroll to employee wallets.
- Cost: 0.5-1.0% per transaction (negotiated by volume).
- Multi-country UEMOA compatible (SN, CI, ML, BF).
- Compliance: separate CSS / IPRES + IR salary declarations.
Automated payroll workflow
`
[Month M: prepare payroll file]
↓
[HR + accountant validation]
↓
[POST Wave Business API: bulk_disbursement]
↓
[Wave processes + sends SMS confirmation to each employee]
↓
[Per-transaction success/failure webhook]
↓
[Auto accounting reconciliation]
↓
[Pay stubs generated + sent WhatsApp/email]
`
Step 1 — data model
`prisma
model Employee {
id String @id @default(cuid())
organizationId String
firstName String
lastName String
email String
phone String
whatsapp String?
ninea String?
cniNumber String @unique
iban String?
walletProvider String
walletNumber String
baseSalary Int
contractType String
startedAt DateTime
endedAt DateTime?
isActive Boolean
}
model PayrollRun {
id String @id @default(cuid())
organizationId String
month String
status String
totalGross Int
totalNet Int
totalTaxes Int
validatedBy String?
validatedAt DateTime?
disbursedAt DateTime?
items PayrollItem[]
}
model PayrollItem {
id String @id @default(cuid())
payrollRunId String
payrollRun PayrollRun @relation(fields: [payrollRunId], references: [id])
employeeId String
employee Employee @relation(fields: [employeeId], references: [id])
baseSalary Int
bonuses Int
overtime Int
grossSalary Int
ipresContribution Int
cssContribution Int
incomeTax Int
netSalary Int
paymentStatus String
walletTransactionId String?
payslipPdfUrl String?
}
`
Step 2 — Senegal net salary computation
In Senegal, 2026 legal deductions:
- IPRES: 5.6% employee (employer 8.4%)
- CSS: 7% employee on capped amount
- Income tax (IR): 0-40% brackets
`ts
export function computeNetSenegal(grossXof: number) {
const ipresEmployee = grossXof * 0.056;
const cssEmployee = Math.min(grossXof, 432000) * 0.07;
const taxableIncome = grossXof - ipresEmployee - cssEmployee;
let incomeTax = 0;
if (taxableIncome > 50000) {
if (taxableIncome <= 75000) incomeTax = (taxableIncome - 50000) * 0.20;
else if (taxableIncome <= 100000) incomeTax = 5000 + (taxableIncome - 75000) * 0.25;
else if (taxableIncome <= 200000) incomeTax = 11250 + (taxableIncome - 100000) * 0.30;
else if (taxableIncome <= 500000) incomeTax = 41250 + (taxableIncome - 200000) * 0.35;
else incomeTax = 146250 + (taxableIncome - 500000) * 0.40;
}
const netSalary = grossXof - ipresEmployee - cssEmployee - incomeTax;
return {
gross: grossXof,
ipres: Math.round(ipresEmployee),
css: Math.round(cssEmployee),
incomeTax: Math.round(incomeTax),
net: Math.round(netSalary),
};
}
`
Step 3 — bulk disbursement via Wave API
`ts
async function disbursePayroll(payrollRunId: string) {
const run = await prisma.payrollRun.findUnique({
where: { id: payrollRunId },
include: { items: { include: { employee: true } } },
});
const waveItems = run.items.filter(i => i.employee.walletProvider === 'WAVE');
const bulkRequest = {
receivers: waveItems.map(item => ({
external_id: item.id,
amount: String(item.netSalary),
currency: 'XOF',
receiver_msisdn: item.employee.walletNumber,
Need a professional website?
Kolonell builds websites that attract clients, optimized for the Sénégalese market. Free quote in 2 minutes.
narration: Salary ${run.month},
})),
};
const res = await fetch('https://api.wave.com/v1/business/bulk-disbursements', {
method: 'POST',
headers: {
'Authorization': Bearer ${process.env.WAVE_BUSINESS_API_KEY},
'Content-Type': 'application/json',
'Idempotency-Key': payroll_${payrollRunId},
},
body: JSON.stringify(bulkRequest),
});
const result = await res.json();
for (const r of result.results) {
await prisma.payrollItem.update({
where: { id: r.external_id },
data: {
paymentStatus: r.status === 'success' ? 'SENT' : 'FAILED',
walletTransactionId: r.transaction_id,
},
});
}
}
`
Idempotency-Key prevents double disbursement on bugs.
Step 4 — confirmation webhooks
`ts
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('wave-signature') ?? '';
const expected = crypto
.createHmac('sha256', process.env.WAVE_BUSINESS_WEBHOOK_SECRET!)
.update(body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return NextResponse.json({ error: 'invalid_signature' }, { status: 401 });
}
const event = JSON.parse(body);
if (event.type === 'disbursement.completed') {
await prisma.payrollItem.update({
where: { id: event.external_id },
data: { paymentStatus: 'RECEIVED' },
});
const item = await prisma.payrollItem.findUnique({
where: { id: event.external_id },
include: { employee: true, payrollRun: true },
});
await sendWhatsApp(item.employee.whatsapp ?? item.employee.phone, {
template: 'payroll_received',
params: [
item.employee.firstName,
item.netSalary.toLocaleString(),
item.payrollRun.month,
],
});
}
return NextResponse.json({ ok: true });
}
`
Step 5 — automatic pay stubs
`ts
import PDFKit from 'pdfkit';
async function generatePayslip(payrollItem) {
const doc = new PDFKit();
const buffers = [];
doc.on('data', buffers.push.bind(buffers));
doc.fontSize(16).text(PAY STUB - ${payrollItem.payrollRun.month});
doc.fontSize(10).text(Employer: ${org.name} (NINEA ${org.ninea}));
doc.text(Employee: ${employee.firstName} ${employee.lastName});
doc.text(ID: ${employee.id});
doc.text(CNI: ${employee.cniNumber});
doc.moveDown();
doc.text('EARNINGS', { underline: true });
doc.text(Base salary: ${item.baseSalary.toLocaleString()} XOF);
doc.text(Bonuses: ${item.bonuses.toLocaleString()} XOF);
doc.text(Overtime: ${item.overtime.toLocaleString()} XOF);
doc.text(GROSS: ${item.grossSalary.toLocaleString()} XOF);
doc.text('DEDUCTIONS', { underline: true });
doc.text(IPRES (5.6%): -${item.ipresContribution.toLocaleString()} XOF);
doc.text(CSS (7%): -${item.cssContribution.toLocaleString()} XOF);
doc.text(IR: -${item.incomeTax.toLocaleString()} XOF);
doc.fontSize(14).text(NET TO PAY: ${item.netSalary.toLocaleString()} XOF, { align: 'right' });
doc.fontSize(9).text(Sent to Wave ${employee.walletNumber} on ${new Date().toLocaleDateString('en-SN')});
doc.end();
const pdfBuffer = Buffer.concat(buffers);
const url = await uploadToSpaces(payslips/${item.id}.pdf, pdfBuffer);
await prisma.payrollItem.update({
where: { id: item.id },
data: { payslipPdfUrl: url },
});
await sendWhatsApp(employee.whatsapp, {
template: 'payslip_available',
params: [employee.firstName, payrollItem.payrollRun.month, url],
});
}
`
Real case — Dakar SME 47 employees
| Metric | Before | After Wave Business B2B |
|---|---|---|
| Payroll prep time/month | 12h | 1h |
| Amount errors | 8% | <1% |
| Salary received delay | 3-5d (transfers) | <5 min (Wave) |
| Monthly processing cost | 65K XOF (bank fees) | 22K XOF (Wave fees) |
| Employee complaints/month | 11 | 1 |
ROI: 11h HR/month savings + 43K XOF/month = ~600K XOF/year equivalent.
Common pitfalls
- Employees without Wave — solution: pay via OM through PayDunya OR bank transfer for banked.
- Employee data exposed — wallet numbers = sensitive. Mandatory encryption + audit logs.
- No 4-eyes validation — PayrollRun >5M XOF must be validated by 2 people (RBAC).
- Accounting reconciliation — need monthly SYSCOHADA accounting export.
- Tax compliance — mandatory monthly IPRES/CSS/IR declarations (DGI eDGI).
FAQ
Q: Wave Business available outside SN/CI?
A: Yes, expanding to ML, BF, OG. Each country has separate account.
Q: Competitors?
A: Orange Money Business (similar), MTN MoMo Bulk Payments (Anglophone Africa). Wave remains cheapest (1% vs 2-3%).
Q: Bank payroll vs Wave?
A: Bank = safe, Wave = fast. For <50 employees, mix Wave (60%) + bank (40% premium executives) ideal.
Conclusion
Wave Business B2B API is the most underused B2B tool in Francophone Africa in 2026. Considerable operational savings for SMEs. 1.5-4M XOF integration investment. 4-8 month ROI. 2027 standard for any SME >30 employees.
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.
