Skip to main content
White Paper

Mobile Money Integration Patterns for Web Applications: A Technical Reference

Technical Patterns for Reliable MoMo Payment Integration in Ghana

6 min read 3 views

Executive Summary

Mobile Money is the primary payment rail for Ghanaian web applications, yet MoMo integrations are frequently implemented without the reliability patterns required for production use. This paper documents six core integration patterns — pre-commit payment records, idempotent webhook handling, HMAC signature verification, active status reconciliation, phone number normalisation, and graceful failure states — with code examples drawn from production PHP applications. Each pattern is explained with both implementation guidance and the failure mode it prevents. Organisations following this reference will build integrations that are resilient to webhook delivery failures, safe from forged callbacks, and recoverable by users without support intervention.

1. Introduction

Mobile Money has become the primary payment rail for consumer and small business transactions in Ghana. MTN Mobile Money, Telecel Cash, and AirtelTigo Money collectively serve tens of millions of active users. For any web application targeting Ghanaian users, MoMo payment support is not optional — it is the baseline expectation.

Despite its ubiquity, MoMo integration is frequently implemented poorly. Point-of-failure analysis of applications built in Ghana reveals recurring patterns: webhook handling without idempotency checks, no reconciliation mechanism, phone number formats that break the API, and failure states that give users no recourse. This paper documents the correct integration patterns and explains why each one matters.

2. Integration Architecture Overview

All three major Ghanaian networks can be accessed through payment aggregators — the most widely used being Hubtel, Korba (now part of Zeepay), and DPO Group. Aggregators handle network routing, reconciliation differences between providers, and provide a single API contract. For most web applications, integrating through an aggregator is significantly simpler than direct network integration and is the recommended approach.

The standard MoMo collection flow through an aggregator has three components:

  1. Initiation: Your server calls the aggregator API with the customer's phone number, amount, and a callback URL
  2. Customer action: The aggregator sends a USSD push to the customer's phone; the customer enters their PIN
  3. Callback: The aggregator POSTs the result (paid or failed) to your callback URL

Each of these steps can fail independently. A robust integration handles failure at each point.

3. Core Integration Patterns

3.1 Pre-Commit Payment Records

Pattern: Create the payment record in your database — with status pending — before calling the aggregator API. The record should include: a UUID reference you generate (do not rely on the provider's ID as your primary key), the amount, network, phone number, and timestamp.

Why it matters: If the initiation API call fails or times out, your payment record still exists. You can retry the initiation later. If you create the record only after a successful API call, a network timeout leaves you with no record of what was attempted.

// Create payment record first
$payment = new Payment();
$payment->reference = Payment::generateReference(); // UUID
$payment->status    = Payment::STATUS_PENDING;
$payment->save();

// Then initiate — even if this fails, the record exists
$momo->initiate($payment, $customerName, $description);

3.2 Idempotent Webhook Handling

Pattern: Your webhook handler must check the current payment status before taking any action. If the payment is already in a terminal state (paid or failed), return HTTP 200 and do nothing.

Why it matters: Aggregators retry webhook delivery if your server returns a non-200 response. Network issues, brief server unavailability, or slow response times can all trigger retries. Without an idempotency check, the same successful payment activates user accounts, delivers credentials, and triggers Moodle enrolments multiple times.

if (!$payment->isPending()) {
    // Already processed — acknowledge and return
    return $this->asJson(['ok' => true]);
}

3.3 Signature Verification

Pattern: Verify the HMAC signature on every incoming webhook before processing it. Hubtel signs payloads using HMAC-SHA256 with a key derived from your client credentials.

Why it matters: Your callback URL is a POST endpoint accessible to the internet. Without signature verification, any actor who discovers the URL can send forged payment confirmations, activating accounts without payment occurring.

$key      = $clientId . $clientSecret;
$expected = base64_encode(hash_hmac('sha256', $rawBody, $key, true));
if (!hash_equals($expected, $receivedSignature)) {
    // Reject — not from Hubtel
    Yii::$app->response->statusCode = 401;
    return $this->asJson(['error' => 'Invalid signature']);
}

3.4 Active Status Reconciliation

Pattern: Run a scheduled job every 15–30 minutes that queries the aggregator's status API for any payment that has been pending for more than N minutes. Do not rely solely on webhooks for final status.

Why it matters: Webhooks are best-effort delivery. Aggregator infrastructure outages, firewall rules that intermittently block incoming connections, or brief server downtime during the webhook delivery window can all result in a confirmed payment that your system never received notification of. A reconciliation job catches these cases before the customer calls support.

3.5 Phone Number Normalisation

Pattern: Normalise all phone numbers to the format expected by your aggregator before storing or transmitting them. For Hubtel, this is the local format: 0XXXXXXXXX (10 digits, starting with 0).

Why it matters: Users submit numbers in international format (+233...), without the leading zero (241234567), with spaces (024 123 4567), and occasionally with hyphens. Sending a malformed number to the aggregator results in an error that the user interprets as "the website doesn't work."

3.6 Graceful Failure States

Pattern: A failed payment should not dead-end the user. The failure screen should display the payment reference, explain the most common failure reasons, offer a resend button (which re-initiates the USSD push for the same pending payment without making the user re-enter details), and provide a support contact.

Why it matters: Failed payments are recoverable. If your UI implies the entire transaction is abandoned, users will re-submit the form and create duplicate registrations, or give up entirely. Neither outcome is acceptable.

4. Security Considerations

4.1 Disable CSRF on the Callback Endpoint

The aggregator's server cannot supply a CSRF token generated by your application. The callback endpoint must have CSRF validation disabled. This is safe because you use signature verification instead — never disable CSRF without signature verification as a replacement control.

4.2 Log Webhook Payloads

Store the raw webhook payload against the payment record. This is essential for dispute resolution when a customer claims payment was made but the system shows failed. The stored payload can be shared with the aggregator's support team for reconciliation.

4.3 Limit the Callback URL's Attack Surface

The callback endpoint should only accept POST requests. It should return 200 for all valid requests (even if processing fails internally) to prevent aggregator retry floods. All error conditions should be handled silently — logged, not exposed in the response body.

5. Testing Strategy

Testing MoMo integration requires careful sequencing:

  1. Unit tests: Test phone normalisation, signature verification, and payment model methods independently
  2. Sandbox integration: Test API call format and response parsing against the aggregator's sandbox — but note that the sandbox does not send real USSD prompts
  3. Production smoke test: Make one small real transaction (GHS 0.01 or the minimum supported) on the live system before opening to customers. The sandbox will not catch real-world issues like carrier-specific number format requirements
  4. Webhook simulation: Use a tool like ngrok to expose a local development server to the internet and receive real webhook callbacks during development

6. Conclusion

MoMo integration is not complicated — but it requires deliberate handling of edge cases that are often overlooked in initial implementations. The patterns documented here — pre-commit records, idempotent webhooks, signature verification, active reconciliation, number normalisation, and graceful failure states — collectively produce an integration that is reliable enough for production use in a commercial web application.

Pinuno Academy covers payment integration as part of the PHP Web Development programme. For organisations that need implementation support or a review of an existing integration, contact us directly.

C

Chrystal Akyempon

Founder at Pinuno Academy — practitioner and instructor in web development, enterprise integration, and ICT training in Ghana.

Related Articles