To accept mobile money through Paystack, initialize a GHS transaction with the mobile_money channel enabled. Via hosted checkout, the customer selects mobile money and enters their phone number. Via the Charge API, POST to /charge with a mobile_money object containing the phone and provider. The customer receives an approval prompt on their phone. Once approved, Paystack fires a charge.success webhook.
Mobile money payments follow an asynchronous approval pattern:
charge.success webhook to your serverThe key difference from card payments: between step 2 and step 5, you are waiting for the customer to act on a separate device. This can take seconds or minutes. Your checkout UI needs to show a waiting state during this period.
The simplest approach. Initialize a GHS transaction with the mobile_money channel:
var response = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 5000, // 50 GHS in pesewas
currency: 'GHS',
channels: ['mobile_money'], // or ['card', 'mobile_money'] for both
callback_url: 'https://yoursite.com/payment/callback',
}),
});
var data = await response.json();
// Redirect to data.data.authorization_url or use the access_code
Paystack's checkout page handles the phone number input, provider selection, and the waiting screen while the customer approves on their phone. You do not need to build any of this UI.
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
For a custom checkout where you collect the phone number and provider in your own UI:
async function chargeMobileMoney(email, amount, phone, provider) {
var response = await fetch('https://api.paystack.co/charge', {
method: 'POST',
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: email,
amount: amount,
currency: 'GHS',
mobile_money: {
phone: phone, // e.g., '0241234567'
provider: provider, // 'mtn', 'atl', or 'vod'
},
}),
});
var data = await response.json();
if (data.data.status === 'send_otp') {
// Customer needs to approve on their phone
return { status: 'pending_approval', reference: data.data.reference };
}
if (data.data.status === 'success') {
return { status: 'success', reference: data.data.reference };
}
return { status: 'failed', message: data.data.gateway_response };
}
// Usage
var result = await chargeMobileMoney(
'customer@example.com',
5000, // 50 GHS
'0241234567',
'mtn'
);
if (result.status === 'pending_approval') {
// Show: "Check your phone and approve the payment"
// Wait for webhook to confirm
}
Provider codes: mtn (MTN MoMo), atl (AirtelTigo), vod (Vodafone Cash).
After the charge is initiated, the customer needs to approve on their phone. This creates a user experience challenge: how long do you wait, and what do you show?
// Frontend: show waiting state and poll for completion
async function waitForApproval(reference, maxWaitSeconds) {
var startTime = Date.now();
var maxWait = (maxWaitSeconds || 120) * 1000; // Default 2 minutes
while (Date.now() - startTime < maxWait) {
var response = await fetch('/api/check-payment?reference=' + reference);
var data = await response.json();
if (data.status === 'success') {
return { paid: true };
}
if (data.status === 'failed') {
return { paid: false, reason: data.reason };
}
// Wait 5 seconds before checking again
await new Promise(function(resolve) { setTimeout(resolve, 5000); });
}
return { paid: false, reason: 'timeout' };
}
Better approach: instead of polling, use a webhook to update your database, and have the frontend poll your database. This way you get the definitive confirmation from Paystack via webhook, not from a verify call that might miss timing.
Show a clear message while waiting: "A payment prompt has been sent to 024****567. Open your phone and enter your mobile money PIN to approve." Include a countdown timer or a loading indicator so the customer knows something is happening.
Mobile money prompts expire after a timeout period set by the provider (typically 1-2 minutes). If the customer does not approve within that window, the charge fails.
Common failure reasons:
When a mobile money charge fails, give the customer the option to try again or switch to a different payment method (card). Do not force them to restart the entire checkout process.
Mobile money transactions use GHS (Ghanaian Cedi) with amounts in pesewas. The conversion is the same as other currencies: multiply the display amount by 100.
// 50 GHS = 5000 pesewas
var amountInPesewas = Math.round(50 * 100); // 5000
// Mobile money providers may have minimum and maximum transaction limits
// These limits vary by provider and account type
// Check Paystack documentation for current limits
Mobile money wallets have balance limits that are lower than bank account limits. A customer might have enough for a 500 GHS purchase on their debit card but not in their mobile money wallet. For high-value transactions, offering card as an alternative is important.
For more on amount handling, see kobo, pesewa, and cents explained.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course