MTN Mobile Money processes billions of dollars annually across Ghana, Nigeria, Uganda, Côte d'Ivoire, and more than a dozen other African markets. If you are building a SaaS product, e-commerce platform, or fintech app for West African users, ignoring MoMo is not a technical choice — it is a business mistake. Yet most payment integration guides default to Stripe, leaving developers to piece together MoMo documentation on their own.
This guide walks you through a production-ready MTN MoMo integration in Node.js, from sandbox credentials to live webhook handling.
Understanding the MoMo API Structure
The MTN MoMo API is organised around three main products:
- Collections — receive payments from customers (the one you need most)
- Disbursements — send money to users or merchants
- Remittances — cross-border transfers
Each product has its own subscription key and base URL. For most apps, you will start with Collections. The API follows REST conventions, uses OAuth 2.0 bearer tokens, and communicates results both synchronously (status codes) and asynchronously (webhooks / callbacks).
Step 1: Set Up Your Sandbox Account
Head to the MTN MoMo Developer Portal and register an account. Once verified:
- Subscribe to the Collections product under the API Marketplace.
- Copy your Primary Key (this is your
Ocp-Apim-Subscription-Key). - Generate sandbox credentials by calling the
/v1_0/apiuserendpoint to create an API user, then/v1_0/apiuser/{referenceId}/apikeyto get an API key.
The sandbox base URL is:
https://sandbox.momodeveloper.mtn.com
Keep your subscription key, API user ID (a UUID you generate), and API key in environment variables — never in source code.
Step 2: Authenticate and Get an Access Token
MoMo uses OAuth 2.0 client credentials flow. Your API user ID and API key are Base64-encoded as userId:apiKey and sent as a Basic auth header.
const axios = require('axios');
const { Buffer } = require('buffer');
require('dotenv').config();
const SUBSCRIPTION_KEY = process.env.MOMO_SUBSCRIPTION_KEY;
const USER_ID = process.env.MOMO_USER_ID;
const API_KEY = process.env.MOMO_API_KEY;
const BASE_URL = 'https://sandbox.momodeveloper.mtn.com';
async function getAccessToken() {
const credentials = Buffer.from(`${USER_ID}:${API_KEY}`).toString('base64');
const response = await axios.post(
`${BASE_URL}/collection/token/`,
{},
{
headers: {
Authorization: `Basic ${credentials}`,
'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
},
}
);
return response.data.access_token;
}
Tokens expire after one hour. Cache the token and refresh it before expiry rather than requesting a new one on every API call — excessive token requests will get your credentials rate-limited in production.
Step 3: Initiate a Payment Request (Request to Pay)
The core Collections endpoint is POST /collection/v1_0/requesttopay. You provide a reference ID (UUID v4), the customer's MSISDN (phone number in international format, e.g. 233XXXXXXXXX for Ghana), and the amount.
const { v4: uuidv4 } = require('uuid');
async function requestPayment({ phoneNumber, amount, currency = 'GHS', note }) {
const token = await getAccessToken();
const referenceId = uuidv4();
await axios.post(
`${BASE_URL}/collection/v1_0/requesttopay`,
{
amount: String(amount),
currency,
externalId: uuidv4(),
payer: { partyIdType: 'MSISDN', partyId: phoneNumber },
payerMessage: note,
payeeNote: note,
},
{
headers: {
Authorization: `Bearer ${token}`,
'X-Reference-Id': referenceId,
'X-Target-Environment': 'sandbox',
'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
'Content-Type': 'application/json',
},
}
);
return referenceId; // store this — you need it to check status
}
A 202 Accepted response means the request was queued, not completed. The actual payment status is delivered asynchronously.
Step 4: Check Payment Status
Poll the status endpoint using the reference ID you stored:
async function getPaymentStatus(referenceId) {
const token = await getAccessToken();
const response = await axios.get(
`${BASE_URL}/collection/v1_0/requesttopay/${referenceId}`,
{
headers: {
Authorization: `Bearer ${token}`,
'X-Target-Environment': 'sandbox',
'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
},
}
);
return response.data.status; // PENDING | SUCCESSFUL | FAILED
}
Do not poll aggressively. A sensible pattern is to check after 10 seconds, then again at 30 seconds, then rely on your webhook for the final state.
Step 5: Handle Webhooks (Callbacks)
When you submit a payment request, you can include a callbackUrl in the request headers. MTN will POST the transaction result to that URL once the customer approves or declines.
The Callback URL Gotcha
This is where most developers lose hours. MTN's sandbox will not call localhost. You must expose a public URL — use ngrok during development.
Additionally, your callback URL must be whitelisted in the developer portal under your API user's profile before it will receive any traffic in production. Missing this step means your webhook never fires, your payment appears stuck in PENDING, and nothing in the logs tells you why.
A minimal Express webhook handler:
app.post('/momo/callback', express.json(), (req, res) => {
const { financialTransactionId, status, externalId } = req.body;
if (status === 'SUCCESSFUL') {
// update your database, fulfil the order
} else {
// log failure, notify user
}
res.sendStatus(200); // always acknowledge
});
Always respond with 200 OK immediately. If your handler throws before responding, MTN may retry — or silently drop the callback.
Common Gotchas Summary
- Currency mismatch: Use
GHSfor Ghana,UGXfor Uganda,XOFfor Francophone markets. Sending the wrong currency code returns a cryptic 400 error. - Phone number format: Always use the international format without the
+prefix (233XXXXXXXXX, not+233...or0...). - UUID uniqueness: The
X-Reference-Idmust be globally unique per request. Reusing one returns a409 Conflict. - Sandbox test numbers: MTN provides specific MSISDN values in the sandbox that simulate success, failure, and timeout scenarios. Use them — do not use real numbers in sandbox mode.
- Production environment header: Switch
X-Target-Environmentfromsandboxtomtncameroon,mtnghana,mtnuganda, etc., depending on your market.
Moving to Production
When you are ready to go live, submit your app for review on the developer portal. You will receive production credentials, a new base URL per market, and access to live callback registration. Expect the review process to take several business days — factor this into your launch timeline.
Why This Matters for Your Project
Mobile money is not a niche payment method in West Africa — it is the default. Building a checkout flow that only supports card payments will exclude a significant portion of your potential user base before they even see your product. Integrating MTN MoMo early, getting the sandbox configuration right, and handling asynchronous payment states robustly will give your app a production-grade payments layer that meets users where they already transact every day.




