Integrating MTN MoMo API Into Your Node.js Backend

Paystack gets all the tutorials. Stripe has entire YouTube channels dedicated to it. Meanwhile, MTN Mobile Money — the payments backbone for hundreds of millions of people across Ghana, Uganda, Cameroon, Rwanda, and beyond — remains frustratingly underdocumented for developers building real products.

That gap costs teams time. This guide closes it.

You will walk away with a working Node.js integration covering sandbox provisioning, API key setup, a collection (payment request) call, and webhook handling — the four things you actually need to go live.


Understanding the MoMo API Architecture

MTN's MoMo API is a REST-based platform built on the Mojaloop open-source framework. It exposes three primary products:

  • Collections — Request payment from a mobile money subscriber
  • Disbursements — Send money out (payouts, refunds)
  • Remittances — Cross-border transfers

For most SaaS and e-commerce backends, Collections is what you need. The API uses OAuth 2.0 bearer tokens with a short expiry (typically 3600 seconds), so your integration must handle token refresh gracefully.


Step 1: Sandbox Setup on the MoMo Developer Portal

Head to momodeveloper.mtn.com and create an account. Once logged in:

  1. Subscribe to the Collections product (free on sandbox).
  2. Note your Subscription Key (called Ocp-Apim-Subscription-Key in headers). This is your API gateway key, not your OAuth client secret — a distinction that trips up most first-time integrators.
  3. Use the portal's sandbox provisioning endpoint to generate a User ID and API Key pair. You will do this programmatically.

Provisioning a Sandbox User

const axios = require('axios');
const { v4: uuidv4 } = require('uuid');

const SUBSCRIPTION_KEY = process.env.MOMO_SUBSCRIPTION_KEY;
const BASE_URL = 'https://sandbox.momodeveloper.mtn.com';

async function provisionSandboxUser() {
  const referenceId = uuidv4(); // This becomes your X-Reference-Id and later your API User ID

  // Step 1: Create API User
  await axios.post(`${BASE_URL}/v1_0/apiuser`, {
    providerCallbackHost: 'https://your-app.com/webhooks/momo'
  }, {
    headers: {
      'X-Reference-Id': referenceId,
      'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
      'Content-Type': 'application/json'
    }
  });

  // Step 2: Create API Key for that user
  const keyResponse = await axios.post(
    `${BASE_URL}/v1_0/apiuser/${referenceId}/apikey`, {}, {
    headers: { 'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY }
  });

  console.log('User ID:', referenceId);
  console.log('API Key:', keyResponse.data.apiKey);
}

provisionSandboxUser();

Run this once, store the referenceId as MOMO_USER_ID and apiKey as MOMO_API_KEY in your .env. You will not need to re-provision unless you rotate credentials.


Step 2: Generating an Access Token

Every API call requires a fresh bearer token. Build a small utility that fetches and caches it:

const Buffer = require('buffer').Buffer;

async function getAccessToken() {
  const credentials = Buffer.from(
    `${process.env.MOMO_USER_ID}:${process.env.MOMO_API_KEY}`
  ).toString('base64');

  const response = await axios.post(
    `${BASE_URL}/collection/token/`,
    {},
    {
      headers: {
        Authorization: `Basic ${credentials}`,
        'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    }
  );

  return response.data.access_token;
}

In production, cache this token in Redis with a TTL of 3500 seconds to avoid an extra round-trip on every payment request.


Step 3: Requesting a Collection (Charge the User)

This is the core call — you push a payment request to the subscriber's phone, they confirm with their MoMo PIN, and MTN settles to your wallet.

async function requestPayment({ amount, currency, phoneNumber, orderId, description }) {
  const token = await getAccessToken();
  const referenceId = uuidv4();

  await axios.post(`${BASE_URL}/collection/v1_0/requesttopay`, {
    amount: String(amount),
    currency,                        // 'EUR' on sandbox; 'GHS', 'UGX', etc. in production
    externalId: orderId,
    payer: {
      partyIdType: 'MSISDN',
      partyId: phoneNumber           // e.g. '233241234567' — international format, no '+'
    },
    payerMessage: description,
    payeeNote: description
  }, {
    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'll use it to check status or match webhooks
}

The response to this call is a 202 Accepted — not a confirmation. Payment is asynchronous. That is where webhooks come in.


Step 4: Handling Webhooks

When the subscriber approves or rejects the payment, MTN POSTs a callback to the providerCallbackHost you registered. Set up an Express endpoint to receive it:

app.post('/webhooks/momo', express.json(), (req, res) => {
  const { referenceId, status, financialTransactionId } = req.body;

  if (status === 'SUCCESSFUL') {
    // Update your order/payment record using referenceId
    // financialTransactionId is MTN's own transaction reference — store it for reconciliation
  } else if (status === 'FAILED') {
    // Notify the user, release any reserved inventory
  }

  res.sendStatus(200); // Always acknowledge quickly — MTN retries on non-200 responses
});

Polling as a Fallback

Webhooks can fail — network issues, server restarts, misconfigured hosts. Build a polling fallback using the GET /collection/v1_0/requesttopay/{referenceId} endpoint. A simple job that polls every 10 seconds for up to 5 minutes covers the vast majority of delayed confirmations.


Production Checklist Before Going Live

Switching from sandbox to production is not just a URL swap. Work through this list:

  • Environment header: Change X-Target-Environment from sandbox to your target market (e.g., mtngh for Ghana, mtnug for Uganda).
  • Currency codes: Sandbox uses EUR; production uses local currency (GHS, UGX, XAF, etc.).
  • Phone number format: Validate MSISDN format per country — Ghana is 233XXXXXXXXX, Uganda is 256XXXXXXXXX.
  • API keys: Re-provision a production API User via the live portal. Sandbox credentials do not carry over.
  • Webhook TLS: Your callback URL must be HTTPS with a valid certificate. Self-signed certs will be rejected.
  • Rate limits: Understand your subscription tier's request quota. Implement exponential backoff on 429 responses.

What About Disbursements?

The Disbursements API follows the same token-and-subscription-key pattern. The key difference is the endpoint (/disbursement/v1_0/transfer) and the direction of funds. If you are building payroll tools, marketplace payout features, or airtime-top-up services, the same foundational code above applies — swap the product subscription key and endpoint path.


Why This Matters for Your Project

Mobile money is not a niche payment method in Africa — it is the dominant one. If your SaaS product, marketplace, or fintech app targets users in Ghana, Uganda, Cameroon, or any of the 17 other markets MTN operates in, shipping without MoMo support means leaving the majority of your potential users without a checkout option. A well-structured integration — with token caching, webhook handling, and a polling fallback — can be production-ready in under a day. The complexity is manageable; the market opportunity is not small.