Jackson Kojo Osei
statusoperationalaccra--:--:--regiongh · uk
pilot

MoMo One-API

One clean API and one webhook format across MTN MoMo, Telecel Cash, AirtelTigo Money and M-Pesa.

What it does

An open-source routing layer over licensed payment providers, so developers write one integration instead of four. It never holds money: collections go straight into the merchant's own account at Paystack or MTN, and the API normalises requests, statuses and webhooks, re-checks every payment with the provider, and flags anything that doesn't add up.

The problem

Every mobile-money network and aggregator has its own API, its own statuses and its own webhook shape. Switching or adding one means rewriting the payment code.

What you get

MoMo One-API sandbox: a GHS 25.00 MTN collection that moved from pending to succeeded, with its webhook events
A collection in the sandbox: pending, then succeeded, with a webhook for each step.
A Telecel collection that failed with the normalised failure code insufficient_funds
Failures come back with one set of codes, whichever provider handled the payment.
A collection waiting for an OTP, with a field to submit it
When a provider needs an OTP, the payment waits in action_required until you send it.
How MoMo One-API works: your app sends one format, the API routes to Paystack or MTN, maps statuses and sends signed webhooks; money moves only between the customer and your provider account
Money only ever moves between the customer and your account at the licensed provider.

How to use it

Run the API next to your app, start with the sandbox, then add your Paystack or MTN keys. Your code sends the same request whichever network the customer is on.

1. Run it with the sandbox

git clone https://github.com/Jacksonoseikojo1122/momo-one-api.git
cd momo-one-api && npm install
SANDBOX=on DEMO=on npm start   # demo page at http://localhost:8789

2. Ask a customer to pay

curl -X POST localhost:8789/v1/collections \
  -H "content-type: application/json" -H "Idempotency-Key: order-1001" \
  -d '{"amount":2500,"currency":"GHS","phone":"024 123 4567","network":"mtn","customer":{"email":"ama@example.com"}}'
# {"id":"pay_...","status":"pending","phone":"233241234567","provider":"sandbox",...}

3. Mark the order paid from the webhook (Node.js)

app.post("/webhooks/momo", express.raw({ type: "application/json" }), async (req, res) => {
  const want = "sha256=" + crypto.createHmac("sha256", process.env.MOMO_WEBHOOK_SECRET!).update(req.body).digest("hex");
  if (req.get("x-signature") !== want) return res.sendStatus(401);
  const event = JSON.parse(req.body.toString());
  if (event.type === "payment.succeeded") await markOrderPaid(event.data.metadata.order_id);
  res.sendStatus(200);
});

4. Switch to real money

PAYSTACK_SECRET_KEY=sk_test_...        # MTN, Telecel, AirtelTigo, M-Pesa
MTN_COLLECTION_USER_ID=... MTN_COLLECTION_API_KEY=... MTN_COLLECTION_SUBSCRIPTION_KEY=...
MOMO_API_KEYS=a-long-random-key       # required with any real provider
WEBHOOK_URL=https://your-app.com/webhooks/momo WEBHOOK_SECRET=...

Documentation

Features

  • One request format for collections and payouts on MTN, Telecel, AirtelTigo and M-Pesa
  • One payment state machine with normalised failure codes across providers
  • Required idempotency keys and our own provider references, so a timeout never means a double charge
  • Provider webhooks verified and always re-checked with the provider
  • Reconciliation that flags mismatches instead of silently overwriting them
  • Signed outgoing webhooks that Webhook Relay can store, de-duplicate and retry
  • Sandbox mode with test numbers for every outcome

What building it covers

  • Payment state machines
  • Reconciliation
  • Provider adapters
  • Event-driven design

Stack

  • TypeScript
  • Node.js
  • Hono
  • PostgreSQL
  • SQLite
  • Vitest
  • Docker

Questions

Does MoMo One-API hold customers' money?

No. Collections go from the customer straight into your own account at a licensed provider (Paystack, or MTN MoMo directly), and payouts come from that account. MoMo One-API only sends instructions and records statuses.

Which mobile money networks does it support?

MTN MoMo, Telecel Cash and AirtelTigo Money in Ghana, and M-Pesa in Kenya. Paystack covers all four; MTN MoMo can also be used directly through MTN's own API.

How do I stop a customer being charged twice?

Every request needs an Idempotency-Key. Sending the same key and body again returns the same payment without calling the provider. Each payment also carries its own reference at the provider, so a timeout is resolved by asking the provider rather than retrying blindly.

What statuses can a payment have?

pending, action_required (the provider needs an OTP), succeeded, failed (with a code such as declined or insufficient_funds) and reversed. The provider's own wording is kept alongside.

Can I test without real money?

Yes. SANDBOX=on adds a pretend provider where the last four digits of the phone number pick the outcome: success, decline, insufficient funds, OTP, reversal or a payment that never finishes.

Is it ready for live payments?

It's a pilot. Every adapter is tested against fake servers built from the providers' published docs, but it hasn't yet been run against MTN's and Paystack's own sandboxes. Do that, and confirm the MTN amount format, before taking real payments.

Is it free?

Yes, it's open source under the MIT licence. You pay your provider's normal fees and need your own agreement with them.

All projects