Mobile Money powers commerce across Africa in a way that card rails simply do not. In Ghana, Uganda, Côte d'Ivoire, and a dozen other markets, MTN MoMo is the payment layer your product must speak fluently. Yet nearly every payment integration tutorial defaults to Stripe. This guide fixes that.

By the end, you will have a working MTN Mobile Money integration inside a React Native app — including sandbox credentials, a request-to-pay flow, webhook handling, and the edge cases that quietly drain startup revenue.


Understanding the MTN MoMo API Architecture

MTN exposes its Mobile Money capabilities through the MoMo Developer API, a REST-based platform built on OAuth 2.0. The API is organized into products:

  • Collections — request payment from a subscriber (the one you need most)
  • Disbursements — send money out (payouts, refunds)
  • Remittances — cross-border transfers

Each product has its own base URL, subscription key, and OAuth token lifecycle. This surprises developers coming from Stripe, where a single secret key covers everything.


Step 1 — Set Up Your Sandbox Credentials

Head to momodeveloper.mtn.com and register. Once approved:

  1. Subscribe to the Collections product.
  2. Copy your Ocp-Apim-Subscription-Key (called the primary key).
  3. Create a sandbox user by calling the provisioning endpoint — this generates an apiUserId and apiKey pair unique to your sandbox.

Do this provisioning step via cURL or Postman first. Many developers skip it and spend hours confused about 401 errors.

# 1. Create a sandbox API user
curl -X POST \
  https://sandbox.momodeveloper.mtn.com/v1_0/apiuser \
  -H "X-Reference-Id: <your-uuid-v4>" \
  -H "Ocp-Apim-Subscription-Key: <your-subscription-key>" \
  -H "Content-Type: application/json" \
  -d '{"providerCallbackHost": "https://yourapp.example.com"}'

# 2. Generate the API key for that user
curl -X POST \
  https://sandbox.momodeveloper.mtn.com/v1_0/apiuser/<your-uuid-v4>/apikey \
  -H "Ocp-Apim-Subscription-Key: <your-subscription-key>"

Store the returned apiKey securely. You now have everything needed to generate Bearer tokens.


Step 2 — Token Management in Your Backend

Never call the MoMo API directly from your React Native client. Your subscription key and API credentials must live server-side. Build a lightweight backend (Node.js/Express, Python/FastAPI — your choice) that:

  1. Accepts a payment initiation request from the app.
  2. Fetches a fresh Bearer token from MoMo's token endpoint (tokens expire in 3600 seconds).
  3. Fires the Collections request and returns a referenceId to the client.

Token caching matters. Fetching a new token on every request adds ~300ms of latency and will eventually hit rate limits. Cache the token in memory or Redis with a TTL of 55 minutes to stay safely inside the 60-minute expiry window.


Step 3 — Initiating a Request to Pay from React Native

Your React Native app collects the user's phone number and the amount, then posts to your backend endpoint — not to MTN directly.

// services/payment.js
export async function requestMoMoPayment(phoneNumber, amount, orderId) {
  const response = await fetch('https://api.yourapp.com/payments/momo/request', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phoneNumber, amount, orderId }),
  });
  const data = await response.json();
  if (!response.ok) throw new Error(data.message || 'Payment initiation failed');
  return data.referenceId; // UUID you generated server-side
}

On the backend, map that call to a Collections /requesttopay POST. The X-Reference-Id header you send becomes the transaction reference — generate a UUID v4 per transaction and persist it in your database immediately before the API call, not after. This is critical.


Step 4 — Webhook Handling (Where Most Integrations Break)

MTN sends a callback to your providerCallbackHost when a transaction completes, fails, or times out. The payload includes the referenceId, status (SUCCESSFUL, FAILED, or PENDING), and a reason code on failure.

Three things that trip up real apps:

1. Callbacks Are Not Guaranteed

MTN's sandbox and even production environments can drop callbacks under load. Never treat a missing callback as a successful payment. Always implement a polling fallback: if your webhook has not fired within 30 seconds of initiation, poll the /requesttopay/{referenceId} endpoint at 10-second intervals, up to three times.

2. Process Callbacks Idempotently

Duplicate callbacks happen. Before crediting a user's account or fulfilling an order, check whether the referenceId has already been processed in your database. A UNIQUE constraint on your transactions table is your first line of defense.

3. Return a 200 Immediately

Your webhook endpoint must respond with HTTP 200 before doing any business logic. If your endpoint times out processing the order, MTN may retry — and now you have a duplicate problem on top of a performance problem. Acknowledge first, process via a background job second.


Step 5 — Reflecting Status Back to the React Native UI

Since payment completion is asynchronous, your app needs a polling or WebSocket strategy to update the UI. A simple approach:

  • After requestMoMoPayment returns a referenceId, start polling your own backend's /payments/status/:referenceId endpoint every 5 seconds.
  • Cap polling at 90 seconds, then show a "check your SMS" fallback message.
  • Use a useEffect cleanup to cancel the interval when the component unmounts.

Avoid showing a spinner indefinitely. Users on mobile networks in Accra or Kampala may be on 2G. Design for the slow-network, delayed-callback reality of African mobile infrastructure.


Edge Cases That Cost Startups Real Money

  • Wrong MSISDN format: MTN expects the international format without the + sign — 233XXXXXXXXX, not +233XXXXXXXXX or 0XXXXXXXXX. Sanitize all inputs on the backend before the API call.
  • Insufficient funds vs. user rejection: These return different reason codes. Surface meaningful error messages — "Transaction declined by user" feels very different from "Insufficient balance."
  • Sandbox vs. production base URLs: They differ. Use environment variables and review them before every deployment. A misconfigured NODE_ENV has sent real transactions to the sandbox more than once.
  • Currency codes: Collections in Ghana use GHS, Uganda uses UGX. Hardcoding GHS in a multi-market app will cause silent failures.

Going to Production

Production access requires submitting your app for MTN's KYC and compliance review. Prepare your privacy policy, business registration documents, and a demo of your integration flow. Approval timelines vary by market — budget two to four weeks. Use that time to harden your webhook handler and write integration tests against the sandbox.


Why This Matters for Your Project

Payment failures are not just a UX problem — every unhandled edge case is direct revenue loss. Building a robust MoMo integration with proper token caching, idempotent webhook handling, and polling fallbacks is the difference between a fintech product that scales and one that leaks transactions quietly into the night. If you are building a SaaS or marketplace for African users, treating Mobile Money as a first-class payment rail — not an afterthought — is a foundational architectural decision worth getting right from day one.