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



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:87892. 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
- Getting started Sandbox, real providers, configuration
- Integration guide Node, Python and PHP; verifying webhooks; Webhook Relay
- How it works Never holding funds, statuses, idempotency, reconciliation
- Providers Paystack and MTN MoMo setup, with official sources
- Sandbox Test numbers for every outcome
- OpenAPI spec Every endpoint, plus the outgoing webhook
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
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.