Mobile money is not a workaround for Africans who lack credit cards. It is the primary payment rail for hundreds of millions of people across the continent. If you are building a SaaS product, e-commerce platform, or any revenue-generating app targeting West or Central Africa, MTN Mobile Money (MoMo) is not optional — it is the checkout button your users will actually click.

Yet most integration tutorials default to Stripe. This guide fills that gap. You will walk through the full MTN MoMo Collections API flow inside a Next.js application: provisioning sandbox credentials, initiating a payment request, polling for status, and handling webhooks reliably — including the parts the official documentation quietly skips over.

Understanding the MTN MoMo API Structure

MTN MoMo exposes several products under its Open API umbrella. The one you need for accepting payments is Collections. There is also Disbursements (sending money out) and Remittances, but this guide focuses on Collections.

The API is hosted on the MoMo Developer Portal (momodeveloper.mtn.com). Before writing a single line of code, you need to:

  1. Create a developer account on the portal.
  2. Subscribe to the Collections product to receive a Subscription Key (Ocp-Apim-Subscription-Key).
  3. Use the sandbox provisioning endpoint to generate an API User and API Key pair.

That last step is where many developers first get stuck. Unlike Stripe, which hands you keys directly from the dashboard, MTN MoMo requires you to programmatically provision your own sandbox user via a POST /v1_0/apiuser call before you can authenticate. Keep your X-Reference-Id (a UUID you generate yourself) — it becomes your API User ID.

Setting Up the Sandbox in Next.js

Install axios or use the native fetch. Create a utility file at lib/momo.ts to centralize your API logic.

// lib/momo.ts
const MOMO_BASE_URL = process.env.MOMO_BASE_URL!; // sandbox: https://sandbox.momodeveloper.mtn.com
const SUBSCRIPTION_KEY = process.env.MOMO_SUBSCRIPTION_KEY!;
const API_USER = process.env.MOMO_API_USER!;
const API_KEY = process.env.MOMO_API_KEY!;

export async function getMoMoToken(): Promise<string> {
  const credentials = Buffer.from(`${API_USER}:${API_KEY}`).toString("base64");
  const res = await fetch(`${MOMO_BASE_URL}/collection/token/`, {
    method: "POST",
    headers: {
      Authorization: `Basic ${credentials}`,
      "Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY,
    },
  });
  if (!res.ok) throw new Error("Failed to fetch MoMo token");
  const data = await res.json();
  return data.access_token;
}

The access token expires in 3600 seconds. Cache it in memory or a short-lived Redis key rather than fetching a new one on every request — unnecessary token calls count against your rate limits.

Initiating a Collection Request

A Collections request asks the subscriber (your customer) to approve a payment on their phone via a USSD prompt or MoMo app notification.

Create a Next.js API route at app/api/payments/request/route.ts:

  • Generate a fresh UUID as the X-Reference-Id — this is your transaction reference.
  • POST to /collection/v1_0/requesttopay with the bearer token and subscription key.
  • Store the reference ID in your database immediately, before the API call returns.

The API returns 202 Accepted, not a completed transaction. This is the most important behavioral difference from synchronous payment APIs. The 202 means the request was queued, not that the customer paid. Do not mark an order as paid at this point.

Polling vs. Webhooks — The Real Trade-off

MTN MoMo supports both status polling and webhook callbacks, and the official docs present them as equivalent. They are not.

Polling is simpler to implement: after receiving the 202, call GET /collection/v1_0/requesttopay/{referenceId} on an interval until the status is SUCCESSFUL, FAILED, or PENDING resolves. Use this approach for low-volume applications or during sandbox testing.

Webhooks are what you want in production. When a transaction completes, MTN posts a callback to a URL you specify in the callbackUrl field of your initial request. In Next.js, that is another API route:

  • Validate the incoming payload structure — MTN does not sign webhooks with an HMAC secret the way Stripe does.
  • Use your stored reference ID to look up the pending transaction in your database.
  • Check that the amount and currency in the callback match what you originally requested. Do not trust the status alone.
  • Update the order status and trigger any downstream logic (email, provisioning, etc.) only after validation passes.

One underdocumented quirk: the callbackUrl must be a publicly accessible HTTPS endpoint. This means you cannot test webhooks on localhost without a tunneling tool like ngrok or Cloudflare Tunnel. Wire this up early in development — it saves significant debugging time later.

Handling the Environment Transition

Moving from sandbox to production requires resubmitting your application through the MTN MoMo portal for approval. A few things change:

  • The base URL switches from sandbox.momodeveloper.mtn.com to the live host for your target market (Ghana, Uganda, Côte d'Ivoire, etc. each have distinct environments).
  • The currency code must match the market: GHS for Ghana, UGX for Uganda, and so on.
  • Sandbox phone numbers (MTN provides test MSISDNs that auto-approve or auto-fail) are no longer valid. You will be transacting against real subscriber accounts.

Store all environment-specific values — base URL, currency, subscription key — in environment variables and never hardcode them. A single misconfigured currency code will cause silent failures that are hard to trace.

Idempotency and Error Recovery

Because the Collections API is asynchronous and network calls can fail, idempotency is non-negotiable. Your reference UUID doubles as your idempotency key — if a request times out, you can safely re-query by that same ID rather than initiating a duplicate charge. Build your database schema and retry logic around this from day one, not as an afterthought.

Also implement a reconciliation job — a scheduled task that queries MTN for any reference IDs that have been in PENDING state longer than a configurable threshold (15–30 minutes is a reasonable ceiling). This catches edge cases where the webhook was never delivered.

Why This Matters for Your Project

If you are shipping a SaaS or consumer product in Africa, the payment layer is not infrastructure you can defer. The developers who build reliable MoMo integrations early — with proper idempotency, webhook validation, and environment parity — are the ones whose products retain users instead of losing them at checkout. Getting this right is a genuine competitive advantage, and it starts with understanding the API on its own terms rather than mapping it onto Stripe assumptions.