Mobile Money in Africa
In West Africa, Mobile Money is THE dominant payment method:
For Keneya, ignoring Mobile Money would have meant ignoring 70% of our potential users.
Orange Money Integration
1. Orange Money REST API
typescript
import axios from 'axios'
class OrangeMoneyService {
private baseURL = 'https://api.orange.com/orange-money-webpay/dev/v1'
private merchantKey: string
private token: string
async getAccessToken() {
const response = await axios.post(`${this.baseURL}/oauth/token`, {
grant_type: 'client_credentials'
}, {
auth: {
username: process.env.ORANGE_CLIENT_ID,
password: process.env.ORANGE_CLIENT_SECRET
}
})
this.token = response.data.access_token
return this.token
}
async initiatePayment(amount: number, phoneNumber: string, orderId: string) {
const payload = {
merchant_key: this.merchantKey,
currency: 'XOF', // CFA Franc
order_id: orderId,
amount: amount,
return_url: 'https://keneya.com/payment/callback',
cancel_url: 'https://keneya.com/payment/cancel',
notif_url: 'https://keneya.com/api/webhooks/orange-money',
lang: 'en',
reference: `KENEYA-${orderId}`
}
const response = await axios.post(
`${this.baseURL}/webpayment`,
payload,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
)
return {
paymentUrl: response.data.payment_url,
paymentToken: response.data.pay_token
}
}
async checkPaymentStatus(payToken: string) {
const response = await axios.post(
`${this.baseURL}/transactionstatus`,
{ pay_token: payToken },
{
headers: {
'Authorization': `Bearer ${this.token}`
}
}
)
return response.data.status // SUCCESS, PENDING, FAILED
}
}2. Webhook Handler
typescript
// webhooks/orange-money.controller.ts
@Controller('webhooks/orange-money')
export class OrangeMoneyWebhookController {
@Post()
async handleWebhook(@Body() payload: any, @Req() req: Request) {
// Verify signature
const signature = req.headers['x-orange-signature']
const isValid = this.verifySignature(payload, signature)
if (!isValid) {
throw new UnauthorizedException('Invalid signature')
}
const { order_id, status, amount } = payload
if (status === 'SUCCESS') {
// Mark consultation as paid
await this.consultationService.markAsPaid(order_id, {
provider: 'orange_money',
amount,
transactionId: payload.txnid
})
// Send notification to patient
await this.notificationService.sendPaymentConfirmation(order_id)
}
return { status: 'ok' }
}
private verifySignature(payload: any, signature: string): boolean {
const secret = process.env.ORANGE_WEBHOOK_SECRET
const computed = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex')
return computed === signature
}
}Moov Money Integration
Moov Money API
typescript
class MoovMoneyService {
private baseURL = 'https://api.moov-africa.com/v1'
async initiatePayment(amount: number, phoneNumber: string) {
const payload = {
amount: amount.toString(),
currency: 'XOF',
customer_phone: phoneNumber,
merchant_transaction_id: `KENEYA-${Date.now()}`,
callback_url: 'https://keneya.com/api/webhooks/moov-money'
}
const response = await axios.post(
`${this.baseURL}/payments/ussd-push`,
payload,
{
headers: {
'Authorization': `Bearer ${process.env.MOOV_API_KEY}`,
'Content-Type': 'application/json'
}
}
)
return {
transactionId: response.data.transaction_id,
status: response.data.status
}
}
}Unified Management
typescript
// payment.service.ts
export class PaymentService {
async processPayment(
provider: 'orange' | 'moov',
amount: number,
phoneNumber: string,
orderId: string
) {
let result
switch (provider) {
case 'orange':
result = await this.orangeMoneyService.initiatePayment(
amount, phoneNumber, orderId
)
break
case 'moov':
result = await this.moovMoneyService.initiatePayment(
amount, phoneNumber
)
break
}
// Save transaction
await this.transactionRepo.create({
orderId,
provider,
amount,
status: 'pending',
createdAt: new Date()
})
return result
}
}Security
1. Amount Validation
typescript
function validateAmount(amount: number): boolean {
// Minimum 100 XOF, maximum 500,000 XOF
if (amount < 100 || amount > 500000) {
throw new Error('Invalid amount')
}
return true
}2. Idempotence
typescript
async function ensureIdempotent(orderId: string) {
const existing = await this.transactionRepo.findByOrderId(orderId)
if (existing && existing.status === 'success') {
throw new Error('Transaction already processed')
}
}3. Rate Limiting
typescript
@UseGuards(ThrottlerGuard)
@Throttle(5, 60) // 5 requests per minute
@Post('payment')
async initiatePayment(@Body() dto: PaymentDto) {
// ...
}User Experience
typescript
// Frontend - React Native
function PaymentScreen() {
const [provider, setProvider] = useState<'orange' | 'moov'>()
const handlePayment = async () => {
try {
const result = await api.post('/payments', {
provider,
amount: consultationFee,
phoneNumber: user.phone
})
if (provider === 'orange') {
// Redirect to Orange Money page
Linking.openURL(result.paymentUrl)
} else {
// Show USSD instructions for Moov
Alert.alert(
'Dial the USSD code',
`Dial *155# and follow instructions`
)
}
// Poll payment status
pollPaymentStatus(result.transactionId)
} catch (error) {
Alert.alert('Error', error.message)
}
}
return (
<View>
<Text>Choose your operator</Text>
<TouchableOpacity onPress={() => setProvider('orange')}>
<Text>Orange Money</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setProvider('moov')}>
<Text>Moov Money</Text>
</TouchableOpacity>
<Button onPress={handlePayment}>Pay {consultationFee} XOF</Button>
</View>
)
}Keneya Results
Conclusion
Mobile Money integration is essential for any African application involving transactions. A good implementation combines security, reliability and smooth user experience.
*A project requiring Mobile Money? [Let's talk](/contact).*