Skip to main content

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 fromEndpointSecond factor
Your own serverPOST /api/v2/payment/approveYour IP address must be whitelisted. No OTP.
The dashboardPOST /api/v1/payment/approve then /approve/confirmAn 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

StatusMeaningWhat to do
403The 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.
404No payout with that reference on this projectCheck you are using the MALIPOPAY reference, not your own.
400Not a payout, already in a terminal state, or the wallet cannot cover amount + feeRead the message. An insufficient balance names the shortfall.
409Someone else approved it at the same momentDo not retry blindly. Read the payment; it is probably already approved.
502Approved and debited, but the channel did not accept the dispatchDo 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 grossFee in the debit block.
  • 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.