Integrating MTN MoMo API Into Your SaaS App: A Practical Guide
If your SaaS product serves users in Ghana, Uganda, Côte d'Ivoire, or anywhere else MTN operates, accepting Mobile Money payments is not optional — it is the payment method. Credit card penetration is low. Bank transfers are slow. MoMo is what your customers have on their phones, and it is what they will use to pay you.
Yet most payment integration tutorials default to Stripe. This guide does not. It is written for engineering teams building real products for African markets, and it covers the parts that the official documentation leaves you to figure out the hard way.
Understanding the MTN MoMo API Structure
The MTN MoMo API is organized around products. For most SaaS billing use cases, you will work with two:
- Collections — to request payments from customers (the one you want for subscriptions and one-time charges)
- Disbursements — to send money out (useful for payouts, refunds, or marketplace settlements)
Each product has its own base URL, its own API user, and its own API key. This trips up a lot of developers early on. Your Collections credentials cannot be used for Disbursements. Treat them as completely separate integrations, even if they share the same MTN MoMo account hierarchy.
Authentication uses OAuth 2.0. You exchange your API user ID and API key for a Bearer token, which expires after one hour. Build token refresh logic from day one — do not hardcode a token and call it done.
Sandbox Setup: The Gotchas Nobody Warns You About
MTN provides a sandbox environment at sandbox.momodeveloper.mtn.com. To use it, you need to:
- Register on the MoMo Developer Portal
- Subscribe to the Collections (or Disbursements) product
- Create an API user and generate an API key using the provisioning endpoint
That third step is where most developers lose an hour. Unlike Stripe, which gives you keys in a dashboard UI, MTN's sandbox requires you to call an API to create your own API user. You send a POST to /v1_0/apiuser with a reference ID you generate yourself (a UUID), then call /v1_0/apiuser/{referenceId}/apikey to retrieve the key.
# Step 1: Create API User (use your Ocp-Apim-Subscription-Key from the portal)
curl -X POST https://sandbox.momodeveloper.mtn.com/v1_0/apiuser \
-H "X-Reference-Id: <your-uuid>" \
-H "Ocp-Apim-Subscription-Key: <your-subscription-key>" \
-H "Content-Type: application/json" \
-d '{"providerCallbackHost": "your-callback-url.com"}'
# Step 2: Retrieve the API Key
curl -X POST https://sandbox.momodeveloper.mtn.com/v1_0/apiuser/<your-uuid>/apikey \
-H "Ocp-Apim-Subscription-Key: <your-subscription-key>"
A few other sandbox quirks to know:
- Phone numbers are faked. Any 10-digit number formatted correctly will work in sandbox. Use
0241234567style numbers. - Transactions are auto-approved. You will not see a real USSD prompt. This means your happy-path testing is smooth, but you need to deliberately test failure scenarios using specific test numbers or by manipulating your request payloads.
- The sandbox is occasionally flaky. If you get unexplained 500 errors, wait a few minutes and retry. This is not a bug in your code.
Making a Collections Request
Once authenticated, requesting a payment from a customer is a single POST to /collection/v1_0/requesttopay. The key fields are the amount, currency, the customer's MSISDN (phone number), and a unique X-Reference-Id you generate per transaction.
{
"amount": "5.00",
"currency": "GHS",
"externalId": "order-8821",
"payer": {
"partyIdType": "MSISDN",
"partyId": "233241234567"
},
"payerMessage": "Payment for Pro Plan - November",
"payeeNote": "SaaS subscription"
}
A successful request returns 202 Accepted — not 200 OK. This is asynchronous by design. The payment has been queued, not completed. Your system must then either poll the status endpoint or wait for a webhook callback to know the final outcome.
Webhook Reliability: Do Not Rely on It Alone
MTN MoMo does support webhook callbacks via the providerCallbackHost you set during API user creation. In theory, when a transaction completes, MTN posts the result to your endpoint. In practice, webhook delivery is inconsistent — especially across different country deployments of the API.
The correct production strategy is a dual approach:
- Accept the webhook and process it when it arrives.
- Poll as a fallback. After initiating a payment, queue a background job (using something like Celery, BullMQ, or Sidekiq) to poll
GET /collection/v1_0/requesttopay/{referenceId}after 30 seconds, then 90 seconds, then 5 minutes. If the status isSUCCESSFULorFAILED, update your records and stop polling.
Never mark a transaction as pending indefinitely. Set a maximum polling window (say, 10 minutes) after which you treat the transaction as expired and prompt the customer to retry.
Handling Failed Transactions Gracefully
Failed MoMo payments happen. Common reasons include insufficient funds, the customer declining the USSD prompt, or a network timeout on the telecoms side. Your application logic needs to account for all of these without breaking the user experience.
Practical recommendations:
- Store every transaction state change in your database with a timestamp. You want a full audit trail:
PENDING→SUCCESSFULorPENDING→FAILED. - Never provision access before confirming payment. Wait for a confirmed
SUCCESSFULstatus from the API before upgrading a user's account tier or issuing a license. - Surface friendly error messages. When a payment fails, tell the customer what happened in plain language. "Your Mobile Money payment was not completed. Please check your balance and try again." beats a raw API error code.
- Implement idempotency on your end. Use your
X-Reference-Idas the idempotency key. If a webhook fires twice for the same transaction (it happens), your handler should be safe to run again without double-crediting or double-provisioning anything.
A Note on Currency and Phone Number Formatting
Each MTN market uses a different currency code — GHS for Ghana, UGX for Uganda, XOF for francophone West Africa. Do not hardcode a single currency. Build this as a configuration variable per deployment region.
Phone numbers must be in international format without the + sign. A Ghanaian number entered as 0241234567 should be stored and sent as 233241234567. Validate and normalize this on input — do not push the formatting responsibility to your API call layer.
Why This Matters for Your Project
If you are building a SaaS product for African users and you have not yet integrated Mobile Money, you are leaving your largest potential revenue stream untapped. MTN MoMo alone covers over 270 million customers across more than 17 markets. Getting the integration right — robust polling, idempotent webhooks, graceful failure handling — is the difference between a payment system your customers trust and one that quietly drops transactions and erodes retention. Build it properly once, and it becomes a genuine competitive advantage over products that only accept cards.





