Mobile Money Payments
Accept MTN, Telecel, and AirtelTigo Mobile Money payments in Ghana.
Mobile Money Payments
Accept payments from all major mobile money providers in Ghana: MTN Mobile Money, Telecel Cash (formerly Vodafone Cash), and AirtelTigo Money.
Supported Providers
| Provider | Code | Phone Prefixes |
|---|---|---|
| MTN Mobile Money | mtn | 024, 054, 055, 059 |
| Telecel Cash | telecel | 020, 050 |
| AirtelTigo Money | airteltigo | 027, 057, 026, 056 |
PayGate automatically detects the provider from the phone number prefix. You can omit the provider field and we'll determine it for you.
How It Works
Create a Payment
Your server creates a payment request with the customer's phone number.
Customer Authorizes
The customer receives a prompt on their phone to enter their PIN and authorize the payment.
Payment Completes
Once authorized, funds are transferred and you receive a webhook notification.
Create a Mobile Money Payment
const payment = await paygate.payments.create({
amount: 5000, // GHS 50.00 in pesewas
currency: 'GHS',
payment_method: 'mobile_money',
phone: '0241234567', // Provider auto-detected as MTN
description: 'Order #1234',
metadata: {
order_id: '1234'
}
})
// Customer will receive authorization prompt
console.log(payment.status) // 'pending'
console.log(payment.id) // 'pay_abc123...'payment = client.payments.create(
amount=5000, # GHS 50.00 in pesewas
currency='GHS',
payment_method='mobile_money',
phone='0241234567',
description='Order #1234',
metadata={'order_id': '1234'}
)
print(payment.status) # 'pending'
print(payment.id) # 'pay_abc123...'$payment = $paygate->payments->create([
'amount' => 5000,
'currency' => 'GHS',
'payment_method' => 'mobile_money',
'phone' => '0241234567',
'description' => 'Order #1234',
'metadata' => ['order_id' => '1234']
]);
echo $payment->status; // 'pending'
echo $payment->id; // 'pay_abc123...'curl -X POST https://api.44.200.142.19.nip.io/v1/payments \
-H "Authorization: Bearer sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "GHS",
"payment_method": "mobile_money",
"phone": "0241234567",
"description": "Order #1234",
"metadata": {
"order_id": "1234"
}
}'Payment Flow
1. Create Payment
When you create a payment, it starts in pending status:
{
"id": "pay_abc123",
"status": "pending",
"amount": 5000,
"currency": "GHS",
"payment_method": "mobile_money",
"provider": "mtn",
"phone": "024****567"
}2. Customer Authorization
The customer receives a USSD prompt or mobile app notification to authorize the payment:
MTN Mobile Money
Payment Request
Amount: GHS 50.00
Merchant: Your Business Name
Enter PIN to confirm3. Payment Completion
Once the customer authorizes:
- Success: Status changes to
succeeded, you receive apayment.succeededwebhook - Failed: Status changes to
failedwith afailure_code - Timeout: If no response within 5 minutes, status changes to
failedwithtimeouterror
Handle Payment Results
Using Webhooks (Recommended)
Set up a webhook endpoint to receive real-time notifications:
app.post('/webhooks', (req, res) => {
const event = Webhook.constructEvent(req.body, signature, secret)
switch (event.type) {
case 'payment.succeeded':
const payment = event.data.object
// Fulfill the order
await fulfillOrder(payment.metadata.order_id)
break
case 'payment.failed':
// Notify customer, allow retry
await notifyPaymentFailed(payment.metadata.order_id)
break
}
res.json({ received: true })
})Using Polling
If webhooks aren't available, you can poll for the payment status:
const checkPayment = async (paymentId: string) => {
const payment = await paygate.payments.retrieve(paymentId)
switch (payment.status) {
case 'succeeded':
return { success: true, payment }
case 'failed':
return { success: false, error: payment.failure_code }
case 'pending':
// Still waiting, check again
await sleep(5000)
return checkPayment(paymentId)
}
}Phone Number Validation
PayGate validates phone numbers before processing. Valid formats:
| Format | Example | Valid |
|---|---|---|
| 10 digits with leading 0 | 0241234567 | Yes |
| International format | +233241234567 | Yes |
| Without leading 0 | 241234567 | Yes |
| With country code | 233241234567 | Yes |
// All these formats are accepted
const validNumbers = [
'0241234567',
'+233241234567',
'233241234567',
'241234567'
]Error Handling
Common mobile money errors:
| Error Code | Description | User Action |
|---|---|---|
insufficient_funds | Account balance too low | Top up and retry |
invalid_phone | Phone number is invalid | Check phone number |
phone_unreachable | Phone is off or no network | Turn on phone, try again |
transaction_declined | User declined or wrong PIN | Try again |
timeout | No response within timeout | Try again |
daily_limit_exceeded | Daily transaction limit reached | Try tomorrow or use another account |
try {
const payment = await paygate.payments.create({
amount: 5000,
currency: 'GHS',
payment_method: 'mobile_money',
phone: '0241234567'
})
} catch (error) {
switch (error.code) {
case 'invalid_phone':
console.error('Please check the phone number')
break
case 'insufficient_funds':
console.error('Please top up your mobile money account')
break
default:
console.error('Payment failed:', error.message)
}
}Best Practices
1. Validate Phone Numbers Early
Check phone numbers on your frontend before creating a payment:
const isValidGhanaPhone = (phone: string) => {
const cleaned = phone.replace(/\D/g, '')
const prefixes = ['024', '054', '055', '059', '020', '050', '027', '057', '026', '056']
if (cleaned.length === 10 && cleaned.startsWith('0')) {
return prefixes.some(p => cleaned.startsWith(p))
}
// Handle international format
if (cleaned.length === 12 && cleaned.startsWith('233')) {
return prefixes.some(p => cleaned.startsWith('233' + p.slice(1)))
}
return false
}2. Set Clear Timeout Expectations
Tell customers they have about 5 minutes to authorize:
Payment prompt sent to 024****567
Please check your phone and enter your PIN to complete payment.
This request will expire in 5 minutes.3. Handle Network Issues
Mobile money relies on telecom networks which can be unreliable. Implement retries:
const createPaymentWithRetry = async (params, maxRetries = 3) => {
for (let i = 0; i < maxRetries; i++) {
try {
return await paygate.payments.create(params)
} catch (error) {
if (error.code === 'network_error' && i < maxRetries - 1) {
await sleep(2000 * (i + 1)) // Exponential backoff
continue
}
throw error
}
}
}4. Use Idempotency Keys
Prevent duplicate charges with idempotency keys:
const payment = await paygate.payments.create({
amount: 5000,
currency: 'GHS',
payment_method: 'mobile_money',
phone: '0241234567'
}, {
idempotencyKey: `order_${orderId}_payment`
})