How to Integrate MTN Mobile Money in PHP – Complete Guide

How to Integrate MTN Mobile Money in PHP – Complete Guide

Mobile Money is the dominant payment method in Africa, with MTN Mobile Money (MoMo) leading the market. If you’re building a PHP application that needs to accept payments or send money in Africa, integrating MTN MoMo is essential.

In this guide, I’ll show you how to integrate MTN Mobile Money using a modern PHP library that makes it simple and type-safe.

What We’ll Build

By the end of this tutorial, you’ll be able to:

  • Accept payments from customers (Collection)
  • Send money to users (Disbursement)
  • Check transaction status
  • Handle callbacks
  • Test everything in sandbox mode

Installation

First, install the library via Composer:

composer require lepresk/momo-api

Requirements: PHP 7.4+ or PHP 8.x

Quick Start: Accepting Payments

Let’s start with the most common use case – accepting a payment from a customer.

1. Setup Collection API

<?php
use LepreskMomoApiMomoApi;

$collection = MomoApi::collection([
    'environment' => 'sandbox', // Use 'mtncongo', 'mtnuganda', etc. for production
    'subscription_key' => 'YOUR_SUBSCRIPTION_KEY',
    'api_user' => 'YOUR_API_USER',
    'api_key' => 'YOUR_API_KEY',
    'callback_url' => 'https://yourdomain.com/callback'
]);

2. Request Payment (Quick Method)

The simplest way to request a payment:

// Request 1000 XAF from phone number 242068511358
$paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-123');

echo "Payment ID: $paymentId"; // UUID v4

3. Check Payment Status

$transaction = $collection->getPaymentStatus($paymentId);

if ($transaction->isSuccessful()) {
    echo "Payment of {$transaction->getAmount()} {$transaction->getCurrency()} received!";
    echo "Payer: {$transaction->getPayer()}";

    // Update your database, fulfill order, etc.
} elseif ($transaction->isFailed()) {
    $reason = $transaction->getReason();

    if ($reason->isNotEnoughFunds()) {
        echo "Customer has insufficient funds";
    } elseif ($reason->isPayerLimitReached()) {
        echo "Customer reached their transaction limit";
    }
}

Advanced: Sending Money (Disbursement)

Need to pay out to users? Use the Disbursement API:

use LepreskMomoApiMomoApi;
use LepreskMomoApiModelsTransferRequest;

$disbursement = MomoApi::disbursement([
    'environment' => 'sandbox',
    'subscription_key' => 'YOUR_SUBSCRIPTION_KEY',
    'api_user' => 'YOUR_API_USER',
    'api_key' => 'YOUR_API_KEY'
]);

// Send money to a user
$transfer = TransferRequest::make('5000', '242068511358', 'SALARY-001');
$transferId = $disbursement->transfer($transfer);

// Check transfer status
$result = $disbursement->getTransferStatus($transferId);

if ($result->isSuccessful()) {
    echo "Transfer of {$result->getAmount()} sent successfully!";
}

Handling Callbacks

MTN MoMo sends asynchronous callbacks when transactions complete. Here’s how to handle them:

<?php
// webhook.php

use LepreskMomoApiModelsTransaction;

// Parse the callback data
$transaction = Transaction::parse($_GET);

if ($transaction->isSuccessful()) {
    // Update order status in database
    $orderId = $transaction->getExternalId();
    $amount = $transaction->getAmount();

    DB::table('orders')
        ->where('id', $orderId)
        ->update(['status' => 'paid', 'amount' => $amount]);

    // Send confirmation email, etc.
} elseif ($transaction->isFailed()) {
    $reason = $transaction->getReason();

    // Log the failure
    error_log("Payment failed: {$reason->getCode()} - {$reason->getMessage()}");
}

http_response_code(200); // Always return 200 to MTN

Testing in Sandbox Mode

Before going to production, test everything in sandbox:

1. Create Sandbox API User

use LepreskMomoApiMomoApi;
use LepreskMomoApiSupportUuid;

$momo = MomoApi::create(MomoApi::ENVIRONMENT_SANDBOX);

// Generate UUID for API user
$uuid = Uuid::v4();

// Create API user
$apiUser = $momo->sandbox('YOUR_SANDBOX_SUBSCRIPTION_KEY')
    ->createApiUser($uuid, 'https://yourdomain.com/callback');

// Create API key
$apiKey = $momo->sandbox('YOUR_SANDBOX_SUBSCRIPTION_KEY')
    ->createApiKey($apiUser);

echo "API User: $apiUsern";
echo "API Key: $apiKeyn";

2. Use These Credentials

Now use the generated credentials for testing in sandbox mode.

Production Deployment

When ready for production:

$collection = MomoApi::collection([
    'environment' => 'mtncongo', // or 'mtnuganda', 'mtnghana', etc.
    'subscription_key' => env('MOMO_SUBSCRIPTION_KEY'),
    'api_user' => env('MOMO_API_USER'),
    'api_key' => env('MOMO_API_KEY'),
    'callback_url' => env('MOMO_CALLBACK_URL')
]);

Supported Countries

The library supports all MTN MoMo markets:

  • Congo
  • Uganda
  • Ghana
  • Cameroon
  • Zambia
  • Ivory Coast
  • Benin
  • South Africa
  • And more…

Best Practices

1. Always Use Environment Variables

// .env
MOMO_SUBSCRIPTION_KEY=your_key_here
MOMO_API_USER=your_user_here
MOMO_API_KEY=your_key_here
MOMO_CALLBACK_URL=https://yourdomain.com/webhook

2. Validate Callbacks via API

Never trust callback data alone. Always verify:

// In your webhook handler
$callbackData = Transaction::parse($_GET);
$paymentId = $callbackData->getExternalId();

// Verify by calling the API
$verified = $collection->getPaymentStatus($paymentId);

if ($verified->isSuccessful()) {
    // NOW you can trust it and process the order
}

3. Handle Errors Gracefully

use LepreskMomoApiExceptionsResourceNotFoundException;
use LepreskMomoApiExceptionsInternalServerErrorException;

try {
    $paymentId = $collection->quickPay('1000', '242068511358', 'ORDER-123');
} catch (ResourceNotFoundException $e) {
    // Payment not found
    log_error("Payment not found: " . $e->getMessage());
} catch (InternalServerErrorException $e) {
    // MTN server error - retry later
    queue_retry($paymentRequest);
}

4. Check Account Balance

Monitor your account balance regularly:

$balance = $collection->getBalance();

if ((int)$balance->getAvailableBalance() < 10000) {
    notify_admin("Low MoMo balance: {$balance->getAvailableBalance()} {$balance->getCurrency()}");
}

Security Tips

  1. Never commit credentials – Use .env files
  2. Validate webhook IPs – Only accept callbacks from MTN IPs
  3. Use HTTPS – For all callback URLs
  4. Log everything – Keep audit trails
  5. Implement idempotency – Use unique externalId for each transaction

Error Handling Reference

Common error reasons you’ll encounter:

$transaction = $collection->getPaymentStatus($paymentId);

if ($transaction->isFailed()) {
    $reason = $transaction->getReason();

    // Check specific errors
    if ($reason->isNotEnoughFunds()) {
        // Customer balance too low
    } elseif ($reason->isPayerLimitReached()) {
        // Daily/monthly limit exceeded
    } elseif ($reason->isPayeeNotFound()) {
        // Invalid phone number
    }

    // Get raw error data
    echo $reason->getCode();    // e.g., "NOT_ENOUGH_FUNDS"
    echo $reason->getMessage(); // Human-readable message
}

Advanced Features

Refunds

use LepreskMomoApiModelsRefundRequest;

// Refund a previous transaction
$refund = RefundRequest::make(
    '1000',                                    // Amount
    '07a461a4-e721-462b-81c6-b9aa2f8abf06',   // Original transaction ID
    'REFUND-001'                               // Your refund reference
);

$refundId = $disbursement->refund($refund);

// Check refund status
$status = $disbursement->getRefundStatus($refundId);

Custom Payment Request

For more control over payment requests:

use LepreskMomoApiModelsPaymentRequest;

$request = new PaymentRequest(
    amount: '2500',
    currency: 'XAF',
    externalId: 'ORDER-456',
    payer: '242068511358',
    payerMessage: 'Payment for premium subscription',
    payeeNote: 'Thank you for your purchase'
);

$paymentId = $collection->requestToPay($request);

Real-World Example: E-commerce Checkout

Here’s a complete checkout flow:

<?php
// checkout.php

use LepreskMomoApiMomoApi;

// 1. Initialize collection
$collection = MomoApi::collection([...]);

// 2. Get cart total from session
$cartTotal = $_SESSION['cart_total'];
$userPhone = $_SESSION['user_phone'];
$orderId = generate_order_id();

// 3. Request payment
try {
    $paymentId = $collection->quickPay(
        (string)$cartTotal,
        $userPhone,
        $orderId
    );

    // 4. Save payment ID to database
    DB::table('orders')->insert([
        'id' => $orderId,
        'payment_id' => $paymentId,
        'amount' => $cartTotal,
        'status' => 'pending',
        'created_at' => now()
    ]);

    // 5. Show waiting page
    redirect("/payment/waiting/$paymentId");

} catch (Exception $e) {
    show_error("Payment failed: " . $e->getMessage());
}
<?php
// webhook.php - Handle MTN callback

$transaction = Transaction::parse($_GET);
$orderId = $transaction->getExternalId();

if ($transaction->isSuccessful()) {
    DB::table('orders')
        ->where('id', $orderId)
        ->update(['status' => 'paid']);

    // Trigger order fulfillment
    dispatch(new FulfillOrderJob($orderId));

    // Send confirmation email
    Mail::to($customer)->send(new OrderConfirmation($orderId));
}

http_response_code(200);

Resources

Conclusion

Integrating MTN Mobile Money in PHP is now straightforward with this library. You get:

  • Type-safe – Full PHPStan level 5 compliance
  • Modern – Fluent API with sensible defaults
  • Tested – 57 tests with 211 assertions
  • Production-ready – Used in real applications
  • Well-documented – Examples for every method

The library handles all the complexity of OAuth tokens, HTTP headers, error handling, and status codes – so you can focus on building your application.

Questions? Drop a comment below or open an issue on GitHub!

Found this helpful? Star the repo on GitHub!

Tags: #php #payment #momo #mobilemoney #africa #api #tutorial #laravel #symfony #backend

Similar Posts