NetSoft Money — API Documentation
Integrate your website or app with NetSoft Money to collect payments via mobile money (Airtel Money & TNM Mpamba) or NetSoftMoney Wallet.
Overview
The NetSoft Money API allows businesses to collect payments from customers via Airtel Money, TNM Mpamba, or NetSoftMoney Wallet in Malawi.
The API provides two payment channels:
| Channel | How It Works | Endpoints Prefix |
|---|---|---|
| Mobile Money | Customer approves a USSD/push prompt on their phone (Airtel Money or TNM Mpamba) | /mobile/ |
| NetSoftMoney Wallet | Customer confirms payment with a 6-digit OTP sent via SMS | /wallet/ |
Mobile Money Flow:
- Your server authenticates with the API using your API token to get a short-lived JWT.
- You initialize a payment — the customer receives a prompt on their phone to approve.
- You poll the verify endpoint to check if the payment succeeded.
Wallet Flow:
- Your server authenticates with the API using your API token to get a short-lived JWT.
- You initialize a payment — an OTP is sent to the customer via SMS.
- The customer gives you the OTP, and you submit it to the
/wallet/confirmendpoint. - The payment is processed instantly upon valid OTP.
Supported Providers:
| Provider | Phone Prefix | Example |
|---|---|---|
| Airtel Money | Starts with 9 |
997123456 |
| TNM Mpamba | Starts with 8 |
888123456 |
Base URL:
Content-Type: All requests and responses use application/json
Authentication
Authentication is a two-step process:
Step 1 — Get a JWT: Send your API token to the /authenticate endpoint. You receive a JWT that is valid for 10 minutes.
Step 2 — Use the JWT: Include the JWT in the Authorization header for all transaction endpoints:
API Token Format:
"Token expired. Please re-authenticate." error if you use an expired token.
Endpoints
Shared — Both Mobile & Wallet
Request Body Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
phone |
string | Yes | Customer's phone number. 9 digits, starting with 9 (Airtel) or 8 (TNM). No leading zero. |
amount |
number | Yes | Amount in MWK (Malawian Kwacha). |
cURL:
Response (200):
transaction_id — you need it to verify the payment status.
Possible Errors:
| Code | Message |
|---|---|
| 400 | "Phone and amount required" |
| 400 | "Phone number must be 9 digits starting with 8 or 9" |
| 401 | "Unauthorized: No token provided" |
| 401 | "Token expired. Please re-authenticate." |
| 503 | "Payment service unavailable: ..." |
cURL:
Response — Successful (200):
Response — Still Processing (200):
Response — Failed (200):
processing for more than 2 minutes, it is automatically marked as failed.
Possible Transaction Statuses:
| Status | Meaning |
|---|---|
successful |
Payment completed. Funds received. |
processing |
Customer has not yet completed or cancelled. Keep polling. |
failed |
Payment was declined, cancelled, or timed out. |
error |
Transaction ID not found (wrong ID or different business). |
cURL:
Response (200):
details endpoint returns the stored status and only contacts the provider if the transaction is still pending. The verify endpoint always contacts the provider for a fresh status check on pending transactions.
Request Body Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
phone |
string | Yes | Customer's phone number. 9 digits, starting with 9 or 8. Must be registered on NetSoft Money. |
amount |
number | Yes | Amount in MWK. Must be greater than 0. Customer must have sufficient balance. |
cURL:
Response (200):
/wallet/confirm endpoint.
Possible Errors:
| Code | Message |
|---|---|
| 400 | "Phone and amount are required" |
| 400 | "Phone number must be 9 digits starting with 8 or 9" |
| 400 | "Amount must be greater than 0" |
| 400 | "User not found. The phone number is not registered on Netsoft Money." |
| 400 | "Insufficient balance. The user does not have enough funds." |
| 401 | "Unauthorized: No token provided" |
| 401 | "Token expired. Please re-authenticate." |
Request Body Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
transaction_id |
string | Yes | The transaction_id returned by the initialize endpoint. |
otp |
string | Yes | The 6-digit OTP the customer received via SMS. |
cURL:
Response — Success (200):
Possible Errors:
| Code | Message |
|---|---|
| 400 | "Transaction ID and OTP are required" |
| 400 | "OTP must be 6 digits" |
| 400 | "Invalid OTP. X attempt(s) remaining." |
| 400 | "OTP has expired. Please request a new code using resend-otp." |
| 400 | "Transaction already completed" |
| 400 | "Transaction locked due to too many failed attempts" |
| 400 | "Insufficient balance. The user's balance changed since initialization." |
Request Body Parameters:
| Field | Type | Required | Description |
|---|---|---|---|
transaction_id |
string | Yes | The transaction_id of the pending transaction. |
cURL:
Response (200):
Possible Errors:
| Code | Message |
|---|---|
| 400 | "Transaction ID is required" |
| 400 | "OTP can only be resent for pending transactions" |
| 400 | "Maximum OTP resend limit reached for this transaction" |
cURL:
Response (200):
Possible Transaction Statuses:
| Status | Meaning |
|---|---|
pending |
Awaiting OTP confirmation from the payer. |
successful |
Payment completed. Funds transferred. |
failed |
Transaction failed. |
locked |
Transaction locked due to too many failed OTP attempts. |
cURL:
Response (200):
Transaction Lifecycle
Mobile Money Flow
Your Server Monet API Customer Phone
│ │ │
│ 1. POST /authenticate │ │
│ ─────────────────────────> │ │
│ <── JWT token ─────────── │ │
│ │ │
│ 2. POST /mobile/ │ │
│ initialize │ │
│ ─────────────────────────> │ 3. USSD/Push Prompt │
│ │ ─────────────────────────> │
│ <── transaction_id ─────── │ │
│ │ │
│ │ 4. Customer enters PIN │
│ │ <───────────────────────── │
│ │ │
│ 5. GET /mobile/ │ │
│ verify/{id} │ │
│ ─────────────────────────> │ │
│ <── status ─────────────── │ │
│ │ │
│ (repeat step 5 until │ │
│ status != "processing") │ │
Mobile Money Status Transitions:
initialized ──> processing ──> successful
│
└──────> failed (cancelled, declined, or 2-min timeout)
Recommended Polling Strategy:
- After initializing, wait 5 seconds before the first verify call.
- Poll every 5–10 seconds.
- Stop polling when status is
successfulorfailed. - Maximum polling time: 2 minutes (the API auto-fails after this).
Wallet (NetSoft) Flow
Your Server Monet API Customer Phone
│ │ │
│ 1. POST /authenticate │ │
│ ─────────────────────────> │ │
│ <── JWT token ─────────── │ │
│ │ │
│ 2. POST /wallet/ │ │
│ initialize │ │
│ ─────────────────────────> │ 3. OTP sent via SMS │
│ │ ─────────────────────────> │
│ <── transaction_id ─────── │ │
│ │ │
│ 4. Collect OTP from │ │
│ customer │ │
│ │ │
│ 5. POST /wallet/confirm │ │
│ {transaction_id, otp} │ │
│ ─────────────────────────> │ │
│ <── status (successful) ── │ │
│ │ │
│ (Optional) If OTP expired: │ │
│ POST /wallet/resend-otp │ New OTP via SMS │
│ ─────────────────────────> │ ─────────────────────────> │
Wallet Status Transitions:
pending ──> successful (valid OTP submitted)
│
├──────> failed (transaction expired)
│
└──────> locked (3 failed OTP attempts)
Error Handling
All error responses follow this format:
Common Error Codes:
| HTTP Code | Meaning | Common Causes |
|---|---|---|
| 400 | Bad Request | Missing required fields, invalid phone format, malformed JSON |
| 401 | Unauthorized | Missing/invalid API token, missing/expired JWT |
| 404 | Not Found | Invalid endpoint, transaction not found |
| 405 | Method Not Allowed | Wrong HTTP method (e.g., GET instead of POST) |
| 503 | Service Unavailable | Airtel/TNM API is down or unreachable |
Testing Guide
Mobile Money — Full Flow Test
Run these commands in order to test the mobile money payment flow:
Step 1 — Check API is up:
Step 2 — Get JWT token:
Copy the token from the response.
Step 3 — Initialize a mobile money payment:
Copy the transaction_id from the response. The phone owner will get a payment prompt.
Step 4 — Check payment status:
Repeat until status is successful or failed.
Step 5 — Get transaction details:
Wallet (NetSoft) — Full Flow Test
Run these commands in order to test the wallet payment flow:
Step 1 & 2: Same as above (health check and authenticate).
Step 3 — Initialize a wallet payment:
Copy the transaction_id. An OTP will be sent to the customer via SMS.
Step 4 — Confirm with OTP:
Replace 123456 with the actual OTP the customer received.
Step 5 — (Optional) Resend OTP:
Step 6 — Verify wallet transaction:
Postman Setup
| # | Name | Method | URL |
|---|---|---|---|
| 1 | Health Check | GET | {{base_url}}health |
| 2 | Authenticate | POST | {{base_url}}authenticate |
| Mobile Money | |||
| 3 | Mobile: Initialize | POST | {{base_url}}mobile/initialize |
| 4 | Mobile: Verify | GET | {{base_url}}mobile/verify/{{transaction_id}} |
| 5 | Mobile: Details | GET | {{base_url}}mobile/details/{{transaction_id}} |
| Wallet (NetSoft) | |||
| 6 | Wallet: Initialize | POST | {{base_url}}wallet/initialize |
| 7 | Wallet: Confirm | POST | {{base_url}}wallet/confirm |
| 8 | Wallet: Resend OTP | POST | {{base_url}}wallet/resend-otp |
| 9 | Wallet: Verify | GET | {{base_url}}wallet/verify/{{transaction_id}} |
| 10 | Wallet: Details | GET | {{base_url}}wallet/details/{{transaction_id}} |
Postman Variables:
| Variable | Value |
|---|---|
base_url |
https://test.netsoftmoney.com/gateway/api/v1/ |
api_token |
Your business API token |
jwt |
(set after authenticate call) |
transaction_id |
(set after initialize call) |
Test Checklist — Mobile Money
- Health check returns
"status": "ok" - Authenticate with valid token returns a JWT
- Authenticate with invalid token returns 401 error
- Mobile Initialize with Airtel number (starts with 9) returns
transaction_id - Mobile Initialize with TNM number (starts with 8) returns
transaction_id - Mobile Initialize with invalid phone (e.g.
1234) returns 400 error - Mobile Initialize without JWT returns 401 error
- Mobile Initialize with expired JWT returns 401 with
"Token expired"message - Mobile Verify a completed payment returns
"status": "successful" - Mobile Verify a pending payment returns
"status": "processing" - Mobile Verify after 2+ minutes of no action returns
"status": "failed"with timeout message - Mobile Verify with non-existent transaction ID returns
"status": "error" - Mobile Details for a completed transaction returns correct status and details
Test Checklist — Wallet (NetSoft)
- Wallet Initialize with registered phone returns
transaction_idand"pending" - Wallet Initialize with unregistered phone returns error
"User not found" - Wallet Initialize with insufficient balance returns
"Insufficient balance"error - Wallet Confirm with valid OTP returns
"status": "successful" - Wallet Confirm with wrong OTP returns
"Invalid OTP"with remaining attempts - Wallet Confirm after 3 wrong attempts returns
"Transaction locked" - Wallet Confirm with expired OTP returns
"OTP has expired" - Wallet Resend OTP for pending transaction sends new code
- Wallet Resend OTP after 3 resends returns
"Maximum OTP resend limit reached" - Wallet Verify for completed transaction returns
"status": "successful" - Wallet Details returns correct status, timestamps, and
"provider": "netsoft_wallet"
Important Notes for Testing
997123456, not 0997123456.
/wallet/resend-otp endpoint to send a new code (max 3 resends). The customer has 3 attempts per OTP.
"Token expired" error, call the authenticate endpoint again.
