Tutorials
Popular article

Mobile Money Payment: Orange Money & Moov Money Integration

Wapiki Team
December 30, 2025
9 min read
Mobile MoneyOrange MoneyMoov MoneyPaymentAPI

Mobile Money in Africa

In West Africa, Mobile Money is THE dominant payment method:

  • 🇲🇱 **Mali**: 70% of transactions are Mobile Money
  • 💳 **Bank cards**: <15% penetration
  • 📱 **Orange Money**: 15M+ users
  • 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

  • 💰 **25,000+ payments** processed
  • ✅ **Success rate**: 96%
  • ⚡ **Average time**: 45 seconds
  • 🔒 **0 fraud** detected
  • 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).*

    Did you like this article?

    Share it with your network!