PayGate

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

ProviderCodePhone Prefixes
MTN Mobile Moneymtn024, 054, 055, 059
Telecel Cashtelecel020, 050
AirtelTigo Moneyairteltigo027, 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 confirm

3. Payment Completion

Once the customer authorizes:

  • Success: Status changes to succeeded, you receive a payment.succeeded webhook
  • Failed: Status changes to failed with a failure_code
  • Timeout: If no response within 5 minutes, status changes to failed with timeout error

Handle Payment Results

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:

FormatExampleValid
10 digits with leading 00241234567Yes
International format+233241234567Yes
Without leading 0241234567Yes
With country code233241234567Yes
// All these formats are accepted
const validNumbers = [
  '0241234567',
  '+233241234567',
  '233241234567',
  '241234567'
]

Error Handling

Common mobile money errors:

Error CodeDescriptionUser Action
insufficient_fundsAccount balance too lowTop up and retry
invalid_phonePhone number is invalidCheck phone number
phone_unreachablePhone is off or no networkTurn on phone, try again
transaction_declinedUser declined or wrong PINTry again
timeoutNo response within timeoutTry again
daily_limit_exceededDaily transaction limit reachedTry 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`
})