Skip to main content

Mobile Money Payments

Accept payments via MTN Mobile Money, Airtel Money, and other African mobile money providers through X-Pay's unified API.

Supported Providers

ProviderCountriesCurrenciesCode
MTN MoMoGhana, Rwanda, UgandaGHS, RWF, UGXmomo, momo_rwanda, momo_uganda
Orange MoneyLiberiaLRD, USDorange, momo_liberia
Airtel MoneyEast AfricaMultipleairtel

How Mobile Money Works

Basic Integration

Create Mobile Money Payment

import XPay from "@xpay/javascript-sdk";

const xpay = new XPay({
apiKey: process.env.XPAY_SECRET_KEY,
merchantId: process.env.XPAY_MERCHANT_ID,
environment: "sandbox",
});

// Ghana MTN Mobile Money
const payment = await xpay.payments.create({
amount: "50.00",
currency: "GHS",
payment_method: "momo",
description: "Order #12345",
payment_method_data: {
phone_number: "+233201234567", // Ghana format
},
metadata: {
order_id: "12345",
customer_id: "cust_abc",
},
});

console.log("Payment ID:", payment.data.id);
console.log("Status:", payment.data.status); // 'pending'
console.log("Reference:", payment.data.reference_id); // MoMo reference
console.log("Instructions:", payment.data.instructions);

Poll for Payment Completion

Mobile Money payments are asynchronous - the customer must confirm on their phone:

// Create payment
const payment = await xpay.payments.create({
amount: "50.00",
currency: "GHS",
payment_method: "momo",
payment_method_data: { phone_number: "+233201234567" },
});

// Poll until customer confirms or times out
try {
const finalPayment = await xpay.payments.pollPaymentStatus(payment.data.id, {
maxAttempts: 30, // Check up to 30 times
intervalMs: 2000, // Every 2 seconds = 60 seconds max
finalStatuses: ["succeeded", "completed", "failed", "cancelled"],
});

if (
finalPayment.status === "succeeded" ||
finalPayment.status === "completed"
) {
console.log("✅ Payment successful!");
console.log("Reference ID:", finalPayment.reference_id);
// Activate subscription, unlock content, etc.
} else {
console.log("❌ Payment failed:", finalPayment.status);
// Show retry option to customer
}
} catch (error) {
console.error("Payment polling timeout:", error);
// Handle timeout - customer may not have responded
}

Alternative: Webhook-Based Flow

For production, use webhooks instead of polling:

// Step 1: Create payment and return immediately
app.post('/api/pay-mobile-money', async (req, res) => {
const payment = await xpay.payments.create({
amount: req.body.amount,
currency: 'GHS',
payment_method: 'momo',
payment_method_data: { phone_number: req.body.phone },
metadata: { order_id: req.body.orderId }
});

// Store payment ID with order
await db.orders.update(req.body.orderId, {
payment_id: payment.data.id,
status: 'pending_payment'
});

res.json({
status: 'pending',
message: 'Check your phone for payment prompt',
paymentId: payment.data.id,
reference: payment.data.reference_id
});
});

// Step 2: Handle webhook when payment completes
app.post('/webhooks/xpay', (req, res) => {
const event = req.body;

if (event.type === 'payment.succeeded') {
const payment = event.data;
const orderId = payment.metadata.order_id;

// Fulfill the order
await db.orders.update(orderId, { status: 'paid' });

// Send confirmation SMS/email to customer
await sendConfirmation(payment.metadata.customer_phone);
}

res.sendStatus(200);
});

Country-Specific Examples

Rwanda (MTN Mobile Money)

const payment = await xpay.payments.create({
amount: "5000", // 5,000 RWF
currency: "RWF",
payment_method: "momo_rwanda",
payment_method_data: {
phone_number: "+250788123456", // Rwanda format: +250 7XX XXX XXX
},
});

Uganda (MTN Mobile Money)

const payment = await xpay.payments.create({
amount: "10000", // 10,000 UGX
currency: "UGX",
payment_method: "momo_uganda",
payment_method_data: {
phone_number: "+256771234567", // Uganda format: +256 7XX XXX XXX
},
});

Liberia (Orange Money)

const payment = await xpay.payments.create({
amount: "25.00", // $25 USD or LRD
currency: "USD", // or 'LRD'
payment_method: "orange",
payment_method_data: {
phone_number: "+231779880047", // Liberia format: +231 XXX XXX XXX
},
});

Phone Number Formats

CountryFormatExample
Ghana+233 XX XXX XXXX+233201234567
Rwanda+250 7XX XXX XXX+250788123456
Uganda+256 7XX XXX XXX+256771234567
Liberia+231 XXX XXX XXX+231779880047
Nigeria+234 XXX XXX XXXX+2348012345678
Phone Number Validation

Always validate phone numbers on your frontend before sending to the API. Use a library like libphonenumber-js to validate and format.

Complete Express.js Example

import express from "express";
import XPay from "@xpay/javascript-sdk";

const app = express();
app.use(express.json());

const xpay = new XPay({
apiKey: process.env.XPAY_SECRET_KEY,
merchantId: process.env.XPAY_MERCHANT_ID,
});

// Create mobile money payment
app.post("/api/mobile-payment", async (req, res) => {
try {
const { amount, phone, country } = req.body;

// Determine payment method and currency based on country
const config = {
ghana: { method: "momo", currency: "GHS" },
rwanda: { method: "momo_rwanda", currency: "RWF" },
uganda: { method: "momo_uganda", currency: "UGX" },
liberia: { method: "orange", currency: "USD" },
}[country];

const payment = await xpay.payments.create({
amount: amount.toString(),
currency: config.currency,
payment_method: config.method,
payment_method_data: { phone_number: phone },
webhook_url: "https://your-app.com/webhooks/xpay",
});

res.json({
success: true,
paymentId: payment.data.id,
status: payment.data.status,
reference: payment.data.reference_id,
message: "Check your phone for the payment prompt",
});
} catch (error) {
res.status(400).json({ success: false, error: error.message });
}
});

// Check payment status
app.get("/api/payment/:id/status", async (req, res) => {
const payment = await xpay.payments.retrieve(req.params.id);
res.json({
status: payment.data.status,
reference: payment.data.reference_id,
});
});

// Webhook handler
app.post("/webhooks/xpay", (req, res) => {
const event = req.body;

switch (event.type) {
case "payment.succeeded":
console.log("✅ MoMo payment succeeded:", event.data.id);
break;
case "payment.failed":
console.log("❌ MoMo payment failed:", event.data.id);
break;
}

res.sendStatus(200);
});

app.listen(3000);

Test Phone Numbers (Sandbox)

NumberResult
+233200000001✅ Instant success
+233200000002❌ Declined (insufficient funds)
+233200000003⏳ Timeout (no response)

Error Handling

try {
const payment = await xpay.payments.create({...});
} catch (error) {
if (error instanceof XPayError) {
switch (error.code) {
case 'INVALID_PHONE_NUMBER':
// Show: "Please enter a valid phone number"
break;
case 'UNSUPPORTED_CARRIER':
// Show: "This phone carrier is not supported"
break;
case 'PROVIDER_ERROR':
// Show: "Mobile money service is temporarily unavailable"
break;
default:
// Show: "Payment failed. Please try again."
}
}
}

Next Steps