Mobile Money Checkout
Integrate Mobile Money checkout (MTN MoMo, Orange Money, Wave, Airtel) into your application in under 30 minutes.
This guide walks you through integrating a complete Mobile Money checkout with Wajub. On the menu: server-side initialization, redirect to the hosted payment page, handling confirmation, and webhooks.
Why hosted checkout?
The Wajub hosted payment page handles for you:
- Automatic operator detection based on the customer's phone number.
- Sending the USSD / push payment notification.
- Automatic fallback to another provider if the operator is unavailable.
- Compatibility with all Mobile Money wallets without additional code.
You only need to initialize the transaction and redirect the customer.
1. Initialize the payment server-side
app.post('/checkout', async (req, res) => {
const { amount, currency, phone, email, orderId } = req.body;
const { transaction, authorization_url } = await wajub.payments.initialize({
amount: parseInt(amount, 10), // major unit
currency, // XOF, XAF, NGN, GHS…
customer: {
email: email || undefined,
phone: phone?.startsWith('+') ? phone : `+${phone}`,
name: req.body.name,
},
description: `Order #${orderId}`,
reference: `ORDER-${orderId}`,
callback: `${process.env.BASE_URL}/payment/return?ref=${orderId}`,
});
// Save the reference in your database
await db.orders.update(orderId, {
trx_reference: transaction.reference,
trx_status: 'pending',
});
// Redirect the customer to the payment page
res.json({ authorization_url });
});https://api.wajub.com/paymentscurl https://api.wajub.com/payments \
-H "Authorization: pk_test.8f2b91c4d7e6a0f3" \
-H "Content-Type: application/json" \
-d '{
"amount": 15000,
"currency": "XOF",
"customer": {
"phone": "+2250100000001",
"email": "kouassi@example.com",
"name": "Kouassi Konan"
},
"description": "Recharge order",
"reference": "ORDER-9041"
}'{
"status": "Created",
"code": 201,
"transaction": {
"id": "trx_01JXXXXXXXXXXXXX",
"status": "pending",
"amount": 15000,
"currency": "XOF",
"channel": "orange.ci",
"sandbox": true
},
"authorization_url": "https://pay.wajub.com/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}2. The hosted payment page
Once redirected to the authorization_url, the customer lands on the Wajub page:
- The phone number is pre-filled (if provided).
- The customer selects or confirms their operator.
- They receive a USSD / push notification on their phone.
- They confirm the payment on their mobile.
- Wajub redirects to your
callbackand emits a webhook.
3. Return callback (safety net)
The browser return is not the source of truth. Use it for user experience, but verify server-side before fulfilling:
app.get('/payment/return', async (req, res) => {
const { ref } = req.query;
const order = await db.orders.findByOrderId(ref);
if (!order?.trx_reference) {
return res.render('error', { message: 'Order not found' });
}
const { transaction } = await wajub.payments.verify(order.trx_reference);
if (transaction.status === 'succeeded') {
res.render('success', { order });
} else if (transaction.status === 'failed') {
res.render('failed', { order, reason: 'Payment declined' });
} else {
// pending or in progress — the customer can wait
res.render('pending', { order, reference: transaction.reference });
}
});4. Confirmation webhook (source of truth)
app.post('/webhooks/wajub',
express.raw({ type: '*/*' }),
(req, res) => {
const event = wajub.webhooks.constructEvent(
req.body,
req.headers['x-wajub-signature'],
process.env.WAJUB_WEBHOOK_SECRET
);
// Immediate ACK
res.sendStatus(200);
if (event.type === 'payment.succeeded') {
fulfillOrder(event.data.reference);
} else if (event.type === 'payment.failed') {
markOrderFailed(event.data.reference, event.data.reason);
}
}
);Only fulfill on payment.succeeded
Only when the payment.succeeded webhook arrives should you deliver the
product or service. The callback return is only a user convenience.
5. Handling failures
Mobile Money payments sometimes fail: wrong number, insufficient funds, unavailable network. Plan for these cases:
// Frontend — after return with failure
if (transaction.status === 'failed') {
showError(`Payment failed: ${transaction.failure_reason}`);
// Offer to retry
retryButton.onclick = async () => {
const { authorization_url } = await fetch('/checkout', {
method: 'POST',
body: JSON.stringify(orderData),
}).then(r => r.json());
window.location.href = authorization_url;
};
}Testing your integration
Use the test numbers to validate each scenario:
| Scenario | Number | Expected status |
|---|---|---|
| Successful payment | +237670000001 | succeeded |
| Insufficient funds | +237670000002 | failed |
| Invalid number | +237670000003 | failed |
| Operator timeout | +237670000004 | failed |
Watch the flow in Konsole
Run a test payment and follow it in Konsole Event Stream. You'll see the initialization, provider selection, operator response, and the emitted webhook.