If your SaaS product targets users in Côte d'Ivoire, Cameroon, Uganda, Zambia, or Ghana, you will hit a hard reality quickly: a significant portion of your potential customers do not pay with cards. They pay with mobile money — and across francophone Africa especially, that means MTN Mobile Money (MoMo). Ignoring this is not a product decision; it is a revenue decision.

This guide cuts through the noise and walks you through what actually matters when integrating the MTN MoMo API: sandbox setup, the OAuth 2.0 authentication flow, initiating a collection (payment request), handling callbacks, and the undocumented edge cases that will cost you hours if you are not warned.


Understanding the MoMo API Product Suite

MTN's Open API platform is organized into distinct products. For most SaaS use cases, you will work with two:

  • Collections — requesting payment from a customer's MoMo wallet (the equivalent of a charge or payment intent).
  • Disbursements — sending money out to a wallet (for payouts, refunds, or marketplace settlements).

There is also a Remittances product for cross-border transfers, but Collections is where most teams start. Each product has its own subscription key, base URL segment, and independent OAuth token.


Step 1: Sandbox Setup

Head to the MTN MoMo Developer Portal and create an account. Once in:

  1. Subscribe to the Collections product under the sandbox environment.
  2. You will receive a Primary Key and Secondary Key — these are your Ocp-Apim-Subscription-Key headers.
  3. Use the portal's built-in sandbox provisioning endpoint to create a sandbox API user and get your X-Reference-Id (a UUID you generate) and apiKey.

This three-step bootstrap — provision user, get API key, then authenticate — trips up a lot of developers who expect a simpler key-secret model. The sandbox user creation is a one-time step, but you must do it programmatically.

// Step 1: Create sandbox API user
const { v4: uuidv4 } = require('uuid');
const axios = require('axios');

const referenceId = uuidv4();
const subscriptionKey = process.env.MOMO_SUBSCRIPTION_KEY;
const baseURL = 'https://sandbox.momodeveloper.mtn.com';

async function provisionSandboxUser() {
  await axios.post(
    `${baseURL}/v1_0/apiuser`,
    { providerCallbackHost: 'https://yourapp.com/webhooks/momo' },
    {
      headers: {
        'X-Reference-Id': referenceId,
        'Ocp-Apim-Subscription-Key': subscriptionKey,
        'Content-Type': 'application/json',
      },
    }
  );

  const { data } = await axios.post(
    `${baseURL}/v1_0/apiuser/${referenceId}/apikey`,
    {},
    { headers: { 'Ocp-Apim-Subscription-Key': subscriptionKey } }
  );

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

Save both referenceId (your userId) and apiKey — they are the credentials you will exchange for an access token.


Step 2: OAuth 2.0 Token Exchange

Every API call requires a Bearer token obtained by Base64-encoding userId:apiKey and posting to the token endpoint. Tokens expire after one hour, so build a caching layer from day one — do not request a new token per transaction.

async function getMoMoToken(userId, apiKey) {
  const credentials = Buffer.from(`${userId}:${apiKey}`).toString('base64');

  const { data } = await axios.post(
    `${baseURL}/collection/token/`,
    {},
    {
      headers: {
        Authorization: `Basic ${credentials}`,
        'Ocp-Apim-Subscription-Key': subscriptionKey,
      },
    }
  );

  return data.access_token; // Cache this for ~55 minutes
}

Pitfall: The token endpoint path differs per product — /collection/token/ for Collections, /disbursement/token/ for Disbursements. Using the wrong path returns a cryptic 401 that looks like a credentials error.


Step 3: Initiating a Collection Request

A collection (payment request) is asynchronous. You fire the request, receive a 202 Accepted, and then either poll or wait for a callback. The X-Reference-Id you supply becomes the transaction reference.

async function requestPayment({ amount, currency, phoneNumber, description }) {
  const transactionRef = uuidv4();
  const token = await getMoMoToken(userId, apiKey);

  await axios.post(
    `${baseURL}/collection/v1_0/requesttopay`,
    {
      amount: String(amount),
      currency,
      externalId: transactionRef,
      payer: { partyIdType: 'MSISDN', partyId: phoneNumber },
      payerMessage: description,
      payeeNote: description,
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        'X-Reference-Id': transactionRef,
        'X-Target-Environment': 'sandbox',
        'Ocp-Apim-Subscription-Key': subscriptionKey,
        'Content-Type': 'application/json',
      },
    }
  );

  return transactionRef; // Use this to poll or match callbacks
}

Pitfall #1: amount must be a string, not a number. Sending an integer results in a validation error that the docs do not clearly flag.

Pitfall #2: In sandbox, use EUR as the currency. In production, use the country-specific currency code (e.g., GHS for Ghana, XOF for Côte d'Ivoire). Mixing these is a common source of test failures.


Step 4: Webhook (Callback) Handling

The MoMo API POSTs a callback to your providerCallbackHost URL when a transaction is completed or fails. The payload contains the transaction status (SUCCESSFUL, FAILED, or PENDING) and your original X-Reference-Id.

Key considerations for production-grade webhook handling:

  • Idempotency is your responsibility. MTN may deliver the same callback more than once. Always check if the transaction reference has already been processed before updating your database.
  • Verify by polling. Do not trust the callback payload alone for high-value transactions. After receiving a callback, call the GET /collection/v1_0/requesttopay/{referenceId} endpoint to confirm the status server-side.
  • Respond fast. Return a 200 OK immediately and process the business logic asynchronously. MoMo's gateway has a short timeout, and a slow handler will trigger retries.
  • No HMAC signature. Unlike Stripe or Paystack, MoMo callbacks do not include a cryptographic signature. Mitigate this by verifying every callback via the polling endpoint before acting on it.

Production Readiness Checklist

Before going live, ensure the following:

  • Switch X-Target-Environment from sandbox to production in all request headers.
  • KYC and compliance approval from MTN is required before production credentials are issued. Start this process early — it can take weeks.
  • Rate limiting applies per subscription key. Cache tokens, batch where possible, and implement exponential backoff on 429 responses.
  • Phone number format must be in international format without the + prefix (e.g., 233244123456 for a Ghanaian number).
  • Error logging should capture the full response body. MTN's error codes are specific and diagnosable, but only if you log them.

What This Means for Your SaaS Product

Mobile money is not a secondary payment channel in most of sub-Saharan Africa — it is the primary one. SaaS founders building for these markets who treat MoMo as an afterthought will find their card-only checkout a conversion wall. The MTN MoMo API is well-structured once you understand its multi-step auth model and asynchronous transaction pattern. The investment in getting it right pays dividends across nine-plus MTN markets simultaneously.

At Code!nk Technologies, we have integrated MoMo Collections and Disbursements into production SaaS platforms serving West and Central African markets. The patterns above reflect real implementation experience — the sandbox quirks, the callback edge cases, and the compliance timelines that catch teams off guard. If you are building a payment-enabled product for Africa, getting MoMo right is not optional. It is the work.