Payment Disbursement Flow
Overview
A disbursement (payout) sends funds from your MALIPOPAY balance to a mobile wallet, bank account or till. Unlike a collection there is no customer to prompt: MALIPOPAY pushes the money.
Creating a payout does not send it. Every payout, on every channel and at every amount, is created in AWAITING_APPROVAL and moves only when it is approved. This is not a threshold that some projects can turn off; it is how payouts work.
There are two ways to approve, and they are different surfaces:
| Approving from | Endpoint | Second factor |
|---|---|---|
| Your own server | POST /api/v2/payment/approve | Your IP address must be whitelisted. No OTP. |
| The dashboard | POST /api/v1/payment/approve then /approve/confirm | An OTP sent to the approver |
The rest of this page describes the API flow. If a human approves each payout in the dashboard instead, you only need step 2.
The three steps
Step 1: Whitelist your server's IP address
Before your code can approve anything, register the public IP your server calls from, in the dashboard under Settings. This is a one-off setup step, not something you do per payout, and it cannot be done with an API key alone: the whitelist endpoints require a dashboard login.
Addresses are matched as exact strings. CIDR ranges are not supported, so register every egress IP your infrastructure can leave from, including each NAT gateway and every replica. See IP Whitelisting.
Step 2: Create the disbursement
curl -X POST https://core-prod.malipopay.co.tz/api/v2/payment/disbursement \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"description": "Mobile disbursement",
"amount": 500,
"service": "mobile",
"account": "255684118011"
}'
The identification model is the same as a collection: service plus account, with provider optional for service: "mobile" and required for everything else. That is the whole required body. reference and beneficiaryName are optional and only exist for your own records.
{
"success": true,
"code": 1120,
"message": "Payment is pending",
"data": {
"reference": "ML009142",
"mode": "PAYOUT",
"status": "AWAITING_APPROVAL",
"amount": 50000,
"currency": "TZS"
}
}
Code 1120 with AWAITING_APPROVAL is the normal, healthy outcome. Keep data.reference: it is the only thing step 3 needs.
A payout that is rejected outright comes back as HTTP 400 with code 1112 and a failureReason. Exceeding your monthly payout cap returns HTTP 402.
Step 3: Approve it
curl -X POST https://core-prod.malipopay.co.tz/api/v2/payment/approve \
-H "apiToken: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "reference": "ML009142" }'
This one call approves and dispatches. There is no separate confirm step and no OTP, because the request came from an address you registered.
{
"success": true,
"code": 1117,
"message": "Payment approved successfully",
"data": {
"reference": "ML009142",
"status": "APPROVED",
"amount": 50000,
"debit": {
"accountId": "6890f1c2a4b19e3f2c7d5a10",
"currency": "TZS",
"balanceBefore": 500000,
"grossFee": 2500,
"totalDebit": 52500,
"balanceAfter": 447500
}
}
}
The debit block is the authoritative record of what left your wallet: the amount plus the fee. Reconcile against totalDebit, not against amount.
Errors on approval
| Status | Meaning | What to do |
|---|---|---|
403 | The calling IP is not whitelisted (or the token is invalid or missing) | Add the IP. Check what your egress address actually is; a proxy or NAT changes it. |
404 | No payout with that reference on this project | Check you are using the MALIPOPAY reference, not your own. |
400 | Not a payout, already in a terminal state, or the wallet cannot cover amount + fee | Read the message. An insufficient balance names the shortfall. |
409 | Someone else approved it at the same moment | Do not retry blindly. Read the payment; it is probably already approved. |
502 | Approved and debited, but the channel did not accept the dispatch | Do not re-submit the payout. It is queued and retried automatically. Poll the reference. |
Re-approving a payout that is already APPROVED is safe and does not pay twice.
Sequence diagram
Status ladder
AWAITING_APPROVAL → APPROVED → PROCESSING → SUCCESSFUL
↘ FAILED
payout.approved fires on approval, then payout.confirmed or payout.failed on the final outcome. See Payment Statuses.
Batch payouts
POST /api/v2/payment/disbursement/batch creates many payouts in one request, and the records may mix channels:
{
"description": "March payouts",
"records": [
{ "provider": "VODACOM", "service": "mobile", "account": "255712345678", "amount": 25000, "beneficiaryName": "Asha Mwinchumu" },
{ "provider": "CRDB", "service": "account", "account": "0150123456789", "amount": 80000, "beneficiaryName": "Coastal Supplies Ltd" }
]
}
provider is required on every batch record, including mobile ones. The auto-detection that makes it optional on a single payout does not apply here.
Each record becomes its own payment in AWAITING_APPROVAL, and each is approved individually by its own reference. A batch is a convenience for creation, not a way to approve in bulk.
Refund semantics
If the operator returns FAILED, MALIPOPAY automatically refunds your balance. The funds are not lost. The balance is restored before the payout.failed webhook fires, so a handler that reads your balance on receipt sees the corrected figure.
Fees, limits and caps
- Payout fee: TZS 2,500 or 2.5% of the amount, whichever is higher, on every rail. It is debited alongside the amount at approval and appears as
grossFeein thedebitblock. - Minimum payout: TZS 5,000. Smaller amounts are rejected, since the fee would otherwise approach or exceed the payout.
- Daily and monthly caps: per project. Read the enforced figures at
GET /api/v1/payment/limits; a monthly cap breach surfaces as HTTP 402 at creation. - Before go-live approval, recipients must be on your Test Recipients whitelist and the daily cap is TZS 100,000. See Testing.