Version 1.0 · Updated April 2026

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:

  1. Your server authenticates with the API using your API token to get a short-lived JWT.
  2. You initialize a payment — the customer receives a prompt on their phone to approve.
  3. You poll the verify endpoint to check if the payment succeeded.

Wallet Flow:

  1. Your server authenticates with the API using your API token to get a short-lived JWT.
  2. You initialize a payment — an OTP is sent to the customer via SMS.
  3. The customer gives you the OTP, and you submit it to the /wallet/confirm endpoint.
  4. 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
The provider is detected automatically from the phone number. You do not need to specify it.

Base URL:

https://test.netsoftmoney.com/gateway/api/v1/

Content-Type: All requests and responses use application/json

Prerequisite Before integrating, make sure you have created a merchant/business account on NetSoft Money and have your API token ready in your account settings.

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:

Authorization: Bearer <your-jwt-token>

API Token Format:

netsoftmoney_live_<64 hex characters> // Example: netsoftmoney_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Token Expiry When the JWT expires after 10 minutes, re-authenticate to get a new one. You will receive a "Token expired. Please re-authenticate." error if you use an expired token.

Endpoints

Shared — Both Mobile & Wallet
GET https://test.netsoftmoney.com/gateway/api/v1/health No Auth Required

cURL:

curl -X GET "https://test.netsoftmoney.com/gateway/api/v1/health"

Response (200):

{ "status": "success", "data": { "status": "ok", "timestamp": "2026-04-01T10:30:00+02:00" } }
POST https://test.netsoftmoney.com/gateway/api/v1/authenticate No Auth Required

Request Body:

{ "api_token": "netsoftmoney_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" }

cURL:

curl -X POST "https://test.netsoftmoney.com/gateway/api/v1/authenticate" \ -H "Content-Type: application/json" \ -d '{"api_token": "netsoftmoney_live_a1b2c3d4e5f6..."}'

Response (200):

{ "status": "success", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires_in": "10 minutes", "token_type": "Bearer" } }
Save the token value. You will use it in the Authorization header for all subsequent requests.

Error Response (401):

{ "status": "error", "message": "Invalid API token" }
Collect payments via Airtel Money and TNM Mpamba. The customer approves a USSD/push prompt on their phone.
POST https://test.netsoftmoney.com/gateway/api/v1/mobile/initialize Auth Required

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:

curl -X POST "https://test.netsoftmoney.com/gateway/api/v1/mobile/initialize" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -d '{"phone": "997123456", "amount": 5000}'

Response (200):

{ "status": "success", "data": { "status": "initialized", "message": "Transaction initialized. Please complete the payment on your phone.", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "provider": "airtel_money", "merchant_phone": "999000111" } } }
Important Save the 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: ..."
GET https://test.netsoftmoney.com/gateway/api/v1/mobile/verify/{transaction_id} Auth Required

cURL:

curl -X GET "https://test.netsoftmoney.com/gateway/api/v1/mobile/verify/6609a3b2e1f4c" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response — Successful (200):

{ "status": "success", "data": { "status": "successful", "message": "Transaction successful", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "provider": "airtel_money", "merchant_phone": "999000111" } } }

Response — Still Processing (200):

{ "status": "success", "data": { "status": "processing", "message": "Transaction is still processing. Please check again later.", "transaction_details": { ... } } }

Response — Failed (200):

{ "status": "success", "data": { "status": "failed", "message": "Transaction failed", "transaction_details": { ... } } }
Timeout If a transaction stays in 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).
GET https://test.netsoftmoney.com/gateway/api/v1/mobile/details/{transaction_id} Auth Required

cURL:

curl -X GET "https://test.netsoftmoney.com/gateway/api/v1/mobile/details/6609a3b2e1f4c" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{ "status": "success", "data": { "status": "successful", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "provider": "airtel_money", "merchant_phone": "999000111" } } }
Difference from Verify The 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.
Collect payments from NetSoftMoney Wallet accounts. The customer confirms with a 6-digit OTP sent via SMS.
POST https://test.netsoftmoney.com/gateway/api/v1/wallet/initialize Auth Required

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:

curl -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/initialize" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -d '{"phone": "997123456", "amount": 5000}'

Response (200):

{ "status": "success", "data": { "status": "pending", "message": "Payment authorization code sent to the customer's phone. Submit the code to confirm payment.", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "merchant_phone": "999000111" } } }
Next Step An OTP has been sent to the customer via SMS. Collect the OTP and submit it to the /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."
POST https://test.netsoftmoney.com/gateway/api/v1/wallet/confirm Auth Required

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:

curl -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/confirm" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -d '{"transaction_id": "6609a3b2e1f4c", "otp": "482910"}'

Response — Success (200):

{ "status": "success", "data": { "status": "successful", "message": "Payment completed successfully", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "merchant_phone": "999000111" } } }

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."
OTP Limits The customer has 3 attempts to enter the correct OTP. After 3 failed attempts, the transaction is locked. The OTP expires after 5 minutes.
POST https://test.netsoftmoney.com/gateway/api/v1/wallet/resend-otp Auth Required

Request Body Parameters:

Field Type Required Description
transaction_id string Yes The transaction_id of the pending transaction.

cURL:

curl -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/resend-otp" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \ -d '{"transaction_id": "6609a3b2e1f4c"}'

Response (200):

{ "status": "success", "data": { "status": "otp_resent", "message": "A new payment authorization code has been sent.", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "resends_remaining": 2 } } }

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"
Resend Limit OTP can be resent a maximum of 3 times per transaction. Each resend generates a new code and resets the 5-minute expiry and attempt counter.
GET https://test.netsoftmoney.com/gateway/api/v1/wallet/verify/{transaction_id} Auth Required

cURL:

curl -X GET "https://test.netsoftmoney.com/gateway/api/v1/wallet/verify/6609a3b2e1f4c" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{ "status": "success", "data": { "status": "successful", "message": "Payment completed successfully", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "provider": "netsoft_wallet", "merchant_phone": "999000111" } } }

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.
GET https://test.netsoftmoney.com/gateway/api/v1/wallet/details/{transaction_id} Auth Required

cURL:

curl -X GET "https://test.netsoftmoney.com/gateway/api/v1/wallet/details/6609a3b2e1f4c" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."

Response (200):

{ "status": "success", "data": { "status": "successful", "transaction_details": { "transaction_id": "6609a3b2e1f4c", "payer_phone": "997123456", "amount": 5000, "provider": "netsoft_wallet", "merchant_phone": "999000111", "created_at": "2026-04-01 10:30:00", "updated_at": "2026-04-01 10:31:15" } } }

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:

  1. After initializing, wait 5 seconds before the first verify call.
  2. Poll every 5–10 seconds.
  3. Stop polling when status is successful or failed.
  4. 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:

{ "status": "error", "message": "Description of what went wrong" }

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:

curl -s "https://test.netsoftmoney.com/gateway/api/v1/health" | json_pp

Step 2 — Get JWT token:

curl -s -X POST "https://test.netsoftmoney.com/gateway/api/v1/authenticate" \ -H "Content-Type: application/json" \ -d '{"api_token": "YOUR_API_TOKEN_HERE"}' | json_pp

Copy the token from the response.

Step 3 — Initialize a mobile money payment:

curl -s -X POST "https://test.netsoftmoney.com/gateway/api/v1/mobile/initialize" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_HERE" \ -d '{"phone": "997123456", "amount": 500}' | json_pp

Copy the transaction_id from the response. The phone owner will get a payment prompt.

Step 4 — Check payment status:

curl -s "https://test.netsoftmoney.com/gateway/api/v1/mobile/verify/TRANSACTION_ID_HERE" \ -H "Authorization: Bearer YOUR_JWT_HERE" | json_pp

Repeat until status is successful or failed.

Step 5 — Get transaction details:

curl -s "https://test.netsoftmoney.com/gateway/api/v1/mobile/details/TRANSACTION_ID_HERE" \ -H "Authorization: Bearer YOUR_JWT_HERE" | json_pp
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:

curl -s -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/initialize" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_HERE" \ -d '{"phone": "997123456", "amount": 500}' | json_pp

Copy the transaction_id. An OTP will be sent to the customer via SMS.

Step 4 — Confirm with OTP:

curl -s -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/confirm" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_HERE" \ -d '{"transaction_id": "TRANSACTION_ID_HERE", "otp": "123456"}' | json_pp

Replace 123456 with the actual OTP the customer received.

Step 5 — (Optional) Resend OTP:

curl -s -X POST "https://test.netsoftmoney.com/gateway/api/v1/wallet/resend-otp" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_JWT_HERE" \ -d '{"transaction_id": "TRANSACTION_ID_HERE"}' | json_pp

Step 6 — Verify wallet transaction:

curl -s "https://test.netsoftmoney.com/gateway/api/v1/wallet/verify/TRANSACTION_ID_HERE" \ -H "Authorization: Bearer YOUR_JWT_HERE" | json_pp
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_id and "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
Phone Format Phone numbers are 9 digits with no leading zero. Use 997123456, not 0997123456.
Mobile Money Timeout Mobile money transactions time out after 2 minutes. If the customer does not act on the prompt, the transaction will be marked as failed on the next verify call.
Wallet OTP Expiry Wallet OTPs expire after 5 minutes. Use the /wallet/resend-otp endpoint to send a new code (max 3 resends). The customer has 3 attempts per OTP.
JWT Expiry JWTs expire after 10 minutes. If you get a "Token expired" error, call the authenticate endpoint again.
Business Isolation Each business can only verify its own transactions. You cannot look up a transaction created by a different business.
Use Small Amounts Use small amounts for testing (e.g., MWK 100–500) to avoid unnecessary costs.

Need Help?

Contact us if you need assistance.

Contact Us