External Collections
Some of your customers will pay you through a channel MALIPOPAY is not connected to: a deposit at a bank we have no integration with, cash at your own counter, a cheque. Until you tell us, the collection reference stays open for the full amount, the bank keeps quoting that amount to the next person who presents the number, and your books on MALIPOPAY understate what you have collected.
POST /api/v1/ledger/external-entries records such a payment against one of your collection references. It reduces what is outstanding on that reference and posts the amount in your own books under External Collections. It never touches your MALIPOPAY wallet, your settlement or any fee, because MALIPOPAY did not receive the money.
This endpoint is switched on per account. Until MALIPOPAY enables it for you, every call answers 403 with code 3154. Ask your account contact to enable it.
What it does, and does not do
| Behaviour | Effect |
|---|---|
| Reduces the outstanding amount on the reference | Yes |
| Bank bill lookups (CRDB and other bank channels) quote the reduced amount | Yes |
| Appears on the reference's breakdown, itemised | Yes |
| Posts to your General Ledger (External Collections against Sales Revenue) | Yes |
| Moves your MALIPOPAY wallet | No |
| Changes your settlement | No |
| Charges a fee | No |
Changes the reference's status | No. Read coverage and outstanding instead (see below) |
Report a payment
POST /api/v1/ledger/external-entries
apiToken: YOUR_API_KEY
Content-Type: application/json
{
"reference": "ML229470",
"type": "CREDIT",
"amount": 60000,
"externalReference": "TCB-8842119",
"description": "Cash deposit at TCB Mwanza branch",
"reason": "Customer paid at the bank counter",
"source": "TCB",
"paidAt": "2026-09-22T09:14:00Z"
}
| Field | Required | Notes |
|---|---|---|
reference | Yes | The MALIPOPAY reference (ML…), or your own reference for the same bill. If your own reference matches more than one open bill you get 409 code 3156; send the MALIPOPAY reference instead. |
type | Yes | CREDIT records money received. DEBIT reverses a credit you reported earlier. |
amount | Yes | Integer, in the currency of the collection. |
externalReference | Yes | Your identifier for the movement, such as the bank transaction id. This is the idempotency key: sending the same one again returns the original entry instead of posting a second. |
description | Yes | What the payment was. Shown in your ledger. |
reason | Yes | Why you are recording it. Kept for audit. |
source | No | Where the money came in, for example TCB, NMB, CASH. Defaults to EXTERNAL. |
paidAt | No | When you received it. Defaults to now. |
Response
201 Created on a new entry. 200 OK with idempotentReplay: true if that externalReference was already posted with the same type and amount.
{
"success": true,
"code": 3150,
"message": "External ledger entry posted successfully",
"data": {
"entryId": "ext_4SkzJJsvYlqHyeq1",
"reference": "ML229470",
"customerReference": "999107GVA4207",
"type": "CREDIT",
"amount": 60000,
"currency": "TZS",
"externalReference": "TCB-8842119",
"source": "TCB",
"journalId": "jrn_4SkzJJs_vYlqHyeq",
"balanceAfter": {
"externalPaid": 60000,
"outstanding": 40000
},
"idempotentReplay": false,
"createdAt": "2026-09-22T09:14:07.412Z"
}
}
balanceAfter.outstanding is what the reference still asks for after this entry. Zero means the bank will now refuse further payments against it.
Reverse a report
If a payment you reported turns out not to have cleared, post a DEBIT for the amount. A debit can never exceed the total you have reported on that reference, so it can only undo your own entries; it cannot touch money that arrived through MALIPOPAY.
{
"reference": "ML229470",
"type": "DEBIT",
"amount": 60000,
"externalReference": "TCB-8842119",
"description": "Deposit TCB-8842119 was reversed by the bank",
"reason": "Bounced deposit"
}
A debit may reuse the credit's externalReference: the idempotency key includes type, so a DEBIT with the same reference is a different entry from the CREDIT.
Rules
- A
CREDITcannot exceed what is outstanding.422code3158. - A
DEBITcannot exceed what you have reported.422code3159. - A
FIXEDamount bill takes only the full outstanding. Bank channels refuse a part payment on a fixed bill, so a part credit would leave a remainder nobody could pay.422code3161tells you the exact amount to send. - Fully paid, failed, cancelled and expired bills are refused.
422code3160. ADEBITon a fully paid bill is still accepted: it corrects the books, but it does not reopen the reference. - Invoice control numbers are not supported yet.
422code3160. - Same
externalReference, different amount or type:409code3157. Use a new reference for a new movement.
Read it back
The breakdown endpoint carries the split:
GET /api/v1/payment/breakdown/ML229470
apiToken: YOUR_API_KEY
{
"summary": {
"requested": 100000,
"paid": 0,
"paidViaMalipopay": 0,
"externalPaid": 60000,
"outstanding": 40000,
"currency": "TZS",
"fullyPaid": false,
"coverage": "PARTLY_COVERED"
},
"externalEntries": [
{
"entryId": "ext_4SkzJJsvYlqHyeq1",
"type": "CREDIT",
"amount": 60000,
"externalReference": "TCB-8842119",
"source": "TCB",
"paidAt": "2026-09-22T09:14:00.000Z"
}
]
}
| Field | Meaning |
|---|---|
paid, paidViaMalipopay | Money that arrived through MALIPOPAY. The same number under both names; paid is kept for older integrations. |
externalPaid | Net of the credits and debits you have reported. |
outstanding | requested - paid - externalPaid, never below zero. |
coverage | OPEN, PARTLY_COVERED, COVERED (reached zero with at least some MALIPOPAY money) or COVERED_EXTERNALLY (reached zero on your reports alone). |
A bill that reaches zero on external reports alone keeps its status (typically PROCESSING or PARTIAL) because no money moved through MALIPOPAY. Bank channels still refuse it. Drive your own logic from outstanding and coverage, not from status.
Two more endpoints, both with apiToken:
GET /api/v1/ledger/external-entries?reference=&type=&fromDate=&toDate=&page=&limit=lists what you have reported, newest first.GET /api/v1/ledger/external-entries/summary?fromDate=&toDate=returnsplatformCollected,externalCredits,externalDebits,externalNetandtotalfor the period, so a settlement conversation can separate what MALIPOPAY holds from what you collected yourself.
Status codes
| HTTP | Code | Meaning |
|---|---|---|
| 201 | 3150 | Entry posted |
| 200 | 3151 | Already posted; the original entry is returned |
| 200 | 3152 | Entries listed |
| 200 | 3153 | Summary generated |
| 403 | 3154 | External ledger entries are not enabled for this account |
| 404 | 3155 | No collection found for that reference |
| 409 | 3156 | Your reference matches more than one open collection; use the MALIPOPAY reference |
| 409 | 3157 | That external reference was already used for a different entry |
| 422 | 3158 | The amount is more than the collection still has outstanding |
| 422 | 3159 | A debit cannot exceed the external collections reported on this reference |
| 422 | 3160 | This collection cannot take an external ledger entry |
| 422 | 3161 | This bill must be paid in full; report the whole outstanding amount |
Known limits
- A
DEBITafter a bill is fully paid corrects your books but does not reopen the reference. - A USSD push you re-send on a part-covered reference still asks the customer for the full amount. Bank bill lookups quote the true outstanding.
- Invoice control numbers are excluded for now.