MTN Mobile Money Integration with EasySwitch¶
Overview¶
MTN Mobile Money (MoMo) is the mobile money service from MTN Group, available in 17 African countries. It enables businesses to collect payments via the MTN MoMo subscriber base. EasySwitch wraps the MTN MoMo API (Collection) behind a unified interface with automatic OAuth2 token management.
Prerequisites¶
- EasySwitch installed (see Installation)
- An MTN MoMo developer account (momodeveloper.mtn.com)
- Your subscription key (
Ocp-Apim-Subscription-Key) - Your API secret and App ID from the MTN developer portal
Supported Features¶
| Feature | MTN MoMo Support |
|---|---|
| Request to Pay | ✅ via send_payment() |
| Transaction Status | ✅ via check_status() |
| Transaction Details | ✅ via get_transaction_detail() |
| Refunds (Disbursement) | ✅ via refund() |
| Cancellation | ❌ Not supported |
| Webhook Validation | ✅ HMAC-SHA256 signature |
| OAuth2 Token Mgmt | ✅ Automatic (refresh on expiry) |
Supported Currencies¶
| Currency | Code | Min |
|---|---|---|
| CFA Franc (BCEAO) | XOF |
50.00 |
| CFA Franc (BEAC) | XAF |
50.00 |
| Ugandan Shilling | UGX |
500.00 |
| Tanzanian Shilling | TZS |
500.00 |
| Kenyan Shilling | KES |
10.00 |
| Rwandan Franc | RWF |
100.00 |
| Zambian Kwacha | ZMW |
1.00 |
| Malawian Kwacha | MWK |
100.00 |
| Burundian Franc | BIF |
100.00 |
| Ethiopian Birr | ETB |
1.00 |
| Botswanan Pula | BWP |
1.00 |
| Zimbabwean Dollar | ZWL |
1.00 |
Setup & Configuration¶
Minimal Configuration¶
from easyswitch import EasySwitch, Provider
config = {
"providers": {
Provider.MTN: {
"api_key": "your_subscription_key",
"api_secret": "your_api_secret",
"callback_url": "https://your-site.com/webhook/mtn",
"environment": "sandbox", # or "production"
"extra": {
"app_id": "your_mtn_app_id",
}
}
},
"default_provider": Provider.MTN,
}
client = EasySwitch.from_dict(config)
Environment Variables (.env)¶
EASYSWITCH_ENABLED_PROVIDERS=mtn
EASYSWITCH_DEFAULT_PROVIDER=mtn
EASYSWITCH_MTN_API_KEY=your_subscription_key
EASYSWITCH_MTN_API_SECRET=your_api_secret
EASYSWITCH_MTN_X_APP_ID=your_mtn_app_id
EASYSWITCH_MTN_CALLBACK_URL=https://your-site.com/webhook/mtn
EASYSWITCH_MTN_ENVIRONMENT=sandbox
client = EasySwitch.from_env()
API Methods¶
1. Request to Pay¶
MTN uses an asynchronous payment model: you submit a request-to-pay, MTN sends a USSD push to the customer's phone, and the customer confirms on their device. The initial response is always PENDING — you must poll for the final status.
from easyswitch import (
TransactionDetail, Currency, TransactionStatus,
TransactionType, CustomerInfo, Provider
)
transaction = TransactionDetail(
transaction_id="pay-mtn-20240704-001",
provider=Provider.MTN,
amount=1500.00,
currency=Currency.XOF,
customer=CustomerInfo(
phone_number="+22990123456", # Subscriber MSISDN
first_name="John",
last_name="Doe",
),
reason="Shopping cart payment",
callback_url="https://your-site.com/webhook/mtn",
)
response = client.send_payment(transaction)
print(f"Transaction UUID: {response.transaction_id}")
print(f"Status: {response.status}") # Always PENDING initially
print(f"Expires at: {response.expires_at}")
# Store transaction_id — you will need it to poll for status
Important: MTN returns a
202 Acceptedimmediately. The actual payment result is delivered asynchronously. You should either poll withcheck_status()or wait for a webhook callback.
2. Check Payment Status¶
Poll for the transaction result:
tx_uuid = "uuid-from-send-payment"
status_response = client.check_status(tx_uuid)
print(f"Status: {status_response.status}")
print(f"Amount: {status_response.amount}")
if status_response.status == TransactionStatus.SUCCESSFUL:
print("Customer confirmed payment!")
elif status_response.status == TransactionStatus.FAILED:
print("Payment was rejected")
elif status_response.status == TransactionStatus.PENDING:
print("Customer hasn't responded yet — try again later")
3. Refund (Disbursement)¶
Refund a successful payment. The SDK automatically checks that the original transaction was successful before proceeding.
# Full refund
refund_response = client.refund(
transaction_id="uuid-from-original-payment",
)
print(f"Refund status: {refund_response.status}")
# Partial refund
refund_response = client.refund(
transaction_id="uuid-from-original-payment",
amount=500.00,
reason="Partial refund for damaged item",
)
4. Get Transaction Details¶
detail = client.get_transaction_detail("uuid-from-original-payment")
print(f"Reference: {detail.reference}")
print(f"Status: {detail.status}")
Webhook Management¶
MTN sends notifications via callback URL. EasySwitch validates them using the API secret.
Webhook Endpoint¶
from flask import Flask, request, jsonify
app = Flask(__name__)
client = EasySwitch.from_env()
@app.route("/webhook/mtn", methods=["POST"])
def mtn_webhook():
payload = request.get_json()
headers = dict(request.headers)
# Parse & validate (signature checked automatically)
event = client.parse_webhook(
payload=payload,
headers=headers,
provider=Provider.MTN,
)
if event.status == TransactionStatus.SUCCESSFUL:
print(f"Payment {event.transaction_id} confirmed!")
# Fulfill order…
return jsonify({"status": "ok"}), 200
Status Mapping¶
| MTN Status | EasySwitch Status | Meaning |
|---|---|---|
pending |
PENDING |
Awaiting customer response |
successful |
SUCCESSFUL |
Customer confirmed payment |
failed |
FAILED |
Payment rejected |
rejected |
FAILED |
Payment rejected by operator |
cancelled |
CANCELLED |
Customer cancelled |
ongoing |
PROCESSING |
Transaction in progress |
timeout |
EXPIRED |
Customer did not respond in time |
Complete Example¶
from easyswitch import (
EasySwitch, Provider, TransactionDetail,
Currency, TransactionStatus, CustomerInfo,
)
import time
client = EasySwitch.from_env()
# 1. Create and send payment
tx = TransactionDetail(
transaction_id="pay-001",
provider=Provider.MTN,
amount=2500.00,
currency=Currency.XOF,
customer=CustomerInfo(phone_number="+22990123456"),
reason="Order #1234",
)
response = client.send_payment(tx)
print(f"Sent payment request, UUID: {response.transaction_id}")
# 2. Poll for status (in production, use webhooks instead)
for attempt in range(5):
time.sleep(5)
status = client.check_status(response.transaction_id)
if status.status != TransactionStatus.PENDING:
break
if status.status == TransactionStatus.SUCCESSFUL:
print("Payment received! ✅")
else:
print(f"Final status: {status.status}")
Limitations¶
- Async only: MTN MoMo is fully asynchronous — no synchronous payment confirmation.
- Polling required: You must either poll or use webhooks to get the final payment result.
- No cancellation: Transactions cannot be cancelled once submitted.
- Callback URL required: MTN strongly recommends a callback URL to receive payment notifications.