Skip to main content

Status Codes

Every webhook MALIPOPAY sends carries a numeric code alongside a human-readable message.

Branch on the code. Show the message. The code is stable and will not change wording; the message is written for a person and may be reworded or translated.

{
"reference": "ML00366",
"status": "FAILED",
"event": "payment.failed",
"service": "Collection",
"code": 91051,
"message": "The customer cancelled the payment prompt."
}

How to read a code​

9 1 051
│ │ └── outcome: 00-49 success, 50-99 failure
│ └──── service: 1 Collection, 2 Disbursement
└────── webhook event codes

A reason keeps the same last two digits in every service. 51 means "the customer cancelled" whether it arrived on a collection or a payout, so code % 100 is meaningful on its own when you do not care which service produced it.

The single most useful check is the success/failure split:

const failed = code % 100 >= 50;

Collection​

CodeEventMeaning
91000payment.confirmedPaid in full
91001payment.confirmedPart paid. Compare amount with what you requested
91010payment.refundedRefunded or reversed
91050payment.failedFailed at the operator, no further detail available
91051payment.failedThe customer cancelled the prompt
91052payment.failedThe customer did not confirm in time
91053payment.failedThe customer does not have enough balance
91054payment.failedThe PIN entered was not accepted
91055payment.failedThe operator rejected the request
91056payment.failedThe operator did not answer in time
91057payment.failedThe number is not reachable on that network
91058payment.failedA transaction limit was reached
91059payment.failedThe customer's account is barred or closed
91060payment.failedThe operator treated this as a duplicate
91061payment.failedWe could not reach the operator
91062payment.failedMALIPOPAY float was insufficient. Contact support

Disbursement​

CodeEventMeaning
92000payout.confirmedCompleted
92002payout.approvedApproved and debited from your wallet
92050payout.failedFailed, no further detail available
92051payout.failedThe customer cancelled
92052payout.failedNot confirmed in time
92053payout.failedInsufficient balance
92054payout.failedThe PIN entered was not accepted
92055payout.failedThe operator rejected the payout
92056payout.failedThe operator did not answer in time
92057payout.failedThe recipient is not reachable on that network
92058payout.failedA transaction limit was reached
92059payout.failedThe recipient's account is barred or closed
92060payout.failedThe operator treated this as a duplicate
92061payout.failedWe could not reach the operator
92062payout.failedMALIPOPAY float was insufficient. Contact support

A failed payout is refunded to your wallet automatically.

Invoicing​

CodeEventMeaning
93000invoice.createdAn invoice was created
93001invoice.updatedAn invoice was changed
93002invoice.paidAn invoice was paid in full
93003invoice.cancelledAn invoice was cancelled

Invoicing has no failure codes: an invoice that is not paid is simply still outstanding, and the payment attempt against it reports its own outcome on the Collection codes above.

invoice.paid fires only when nothing is left outstanding. A part payment against an invoice does not fire it, because you have not been settled; that payment arrives as payment.confirmed carrying the amount that moved, and the invoice block on it tells you what is still owed.

Every invoice event carries an extra invoice object, because an invoice has three amounts where a payment has one:

{
"code": 93002,
"event": "invoice.paid",
"service": "Invoicing",
"message": "The invoice was paid in full.",
"reference": "INV-0042",
"amount": 120000,
"invoice": {
"invoiceNo": "INV-0042",
"total": 120000,
"amountPaid": 120000,
"amountDue": 0,
"currency": "TZS",
"dueDate": "2026-10-01",
"paidDate": "2026-09-12T08:14:22.000Z",
"paymentReference": "MI24099"
}
}

The top-level amount is the invoice TOTAL on every invoice event, including invoice.paid. Read invoice.amountPaid for what has been received and invoice.amountDue for what is still owed. The invoice key is present only on invoice.* events; a Collection or Disbursement body is unchanged.

Bulk SMS​

CodeEventMeaning
94000sms.deliveredThe message reached the handset
94050sms.failedNot delivered, no further detail available
94055sms.failedThe operator rejected the message
94056sms.failedThe operator gave up before it could be delivered

That is the whole list, and it is short on purpose. The other offsets in the shared failure vocabulary (51 cancelled, 53 insufficient balance, 57 not reachable, and so on) are not published for Bulk SMS, because an SMPP delivery report does not carry enough to tell them apart and MALIPOPAY does not guess. A wrong reason is worse than no reason when you may automate on it. If richer per-rail detail becomes available the codes will be added here, using the same offsets they already have in every other band.

reference on an SMS event is the message id from the send, and customerReference is the recipient's number. An SMS moves no money, so amount is always 0.

These fire on the operator's TERMINAL delivery report, not when the message is accepted. A message the operator has taken but not yet delivered produces nothing, which is deliberate: there is no "in flight" event to wait for.

Some messages never reach a terminal state at all, because an operator can send a report we do not recognise, and MALIPOPAY leaves those alone rather than guessing. Do not build a flow that blocks until sms.delivered arrives. Treat it as confirmation when it comes, not as a step that is owed to you.

Anything that is not an explicit delivery reports as sms.failed. That is on purpose: a terminal state added by an operator should reach you as a failure you can see and ask about, rather than being dropped or announced as a delivery.

Which codes you can act on​

Most failure codes tell you whether retrying is worth anything:

CodeRetry?
91052, 91056, 91061Yes. Nobody decided anything; the attempt did not complete
91051, 91054Only if the customer asks. They cancelled or mistyped
91053, 91058Not immediately. The money or the headroom is not there
91057, 91059No. Fix the number or have the customer contact their operator
91060No. Check whether an earlier attempt already succeeded

91060 means the operator saw this as a repeat. Before retrying, check the payment with GET /api/v1/payment/reference/{reference} — an earlier attempt may already have been paid.

Handling them​

app.post("/webhooks/malipopay", (req, res) => {
res.sendStatus(200); // acknowledge first, work after

const { code, reference, message } = req.body;

if (code < 91050) return fulfil(reference); // any success
if ([91052, 91056, 91061].includes(code)) return scheduleRetry(reference);

return showCustomer(reference, message); // message is safe to display
});

WhatsApp​

WhatsApp messages have their own band and their own delivery system. They are not configured on the Webhooks screen, and they do not use the payment webhook's signature scheme. See WhatsApp Webhooks.

CodeEventMeaning
95000whatsapp.message.sentAccepted by WhatsApp
95001whatsapp.message.deliveredDelivered to the handset
95002whatsapp.message.readRead by the recipient
95003whatsapp.message.receivedAn inbound message from a customer
95050whatsapp.message.failedFailed, with no recognised cause
95057whatsapp.message.failedThe number cannot receive WhatsApp messages
95058whatsapp.message.failedA WhatsApp rate limit was reached
95059whatsapp.message.failedThe account is locked or restricted
95063whatsapp.message.failedThe 24 hour service window has closed
95064whatsapp.message.failedThe recipient opted out, or Meta withheld it
95065whatsapp.message.failedThe template was rejected, missing, or its parameters did not match
95066whatsapp.message.failedA billing problem on the business account. Contact support

95063 is the one worth handling deliberately. It means the free-form window has closed and the customer must be reached with an approved template instead. It is the most common WhatsApp failure and it is not an error on your side.

A WhatsApp webhook URL is set per phone number through the API, not on the Webhooks screen. If you are not receiving WhatsApp events, that is almost certainly why.

Everything in this guide now fires​

Every code above is emitted by the platform today. Invoicing (93xxx), payout.approved (92002) and Bulk SMS (94xxx) were all on a "not yet emitted" list until 2026-09-12 and have since shipped.

If you subscribed to any of them before that date, no action is needed: the subscription was already stored and deliveries simply begin arriving.

API responses are a different set​

REST responses carry their own code in the envelope:

{ "success": true, "code": 1109, "message": "Collection request sent successfully", "data": {} }

These belong to the API, not to webhook events, and the two spaces do not overlap.

Each MALIPOPAY service owns its own range, so one number always means one thing:

RangeService
1xxxCore payments API
2xxxxPayment gateway
3xxxxMessaging
4xxxxAuthentication
5xxxxAPI gateway
6xxxxShort links
7xxxxUSSD
9xxxxWebhook events (this page)

You will normally only see the 1xxx range, since that is the API you call. A code from another range means the failure came from a service further in, and the message will say what happened.

See Error Handling for the HTTP statuses.

Stability​

A code, once published here, keeps its meaning. New codes are added for new outcomes; existing ones are not repurposed.

If you receive a code that is not on this page, treat it by its band: code % 100 >= 50 is a failure, and message will still say what happened. That is the intended way to stay forward-compatible.