Skip to main content

Testing Card Payments

Card payments are tested in two places, and they are not the same kind of test:

UAT (sandbox)Production (live)
WhereAPI https://core-uat.malipopay.co.tz, dashboard and checkout https://demo.malipopay.co.tzAPI https://core-prod.malipopay.co.tz, dashboard and checkout https://app.malipopay.co.tz
CardsTest cards only, from the table belowReal cards only
MoneyNone moves. The test gateway simulates the bankReal money. The card is charged and the money reaches your Malipopay wallet
3D SecureNot available. No challenge screen appearsThe issuer's challenge appears for enrolled cards

Do the full scenario list on UAT first. Then, on production, run a few small real payments with your own card to prove the live path, including the 3D Secure challenge that UAT cannot show.

Never use test cards on production, and never use real cards on UAT

A test card on production is refused by the live gateway. A real card on UAT does nothing useful and puts real card details into a test system. Keep them apart.

UAT (sandbox)​

UAT is a separate environment with its own dashboard. Use the API key of a project on the UAT dashboard (https://demo.malipopay.co.tz), call the UAT API base URL, and load the card element from https://demo.malipopay.co.tz/embed/card.js. A project on UAT is in test mode, so its card payments go to the Mastercard test gateway and no money moves.

Rules of the sandbox​

  • 3D Secure is switched off on the sandbox. Every card, including cards meant to trigger a challenge, answers AUTHENTICATION_NOT_SUPPORTED and is captured without a challenge. You cannot test the issuer's challenge screen on UAT. Test it on production (see Live).
  • The test bank decides by amount. On this sandbox an approving card approves at TZS 1,000 or less and is declined above it. The amounts in the table are the ones that were run.
  • Pace your retries. The gateway's fraud engine watches how fast and how often the same card is used. A few quick payments in a row on one card (about four, or roughly one a second) and it starts refusing that card with gatewayCode: BLOCKED ("This card could not be accepted for this payment") before the test bank is even asked. Other cards keep working. Leave a few seconds between attempts, rotate between the approving cards below, and if a card is blocked, switch cards or wait about ten minutes. It is not a fault in your integration.
  • The UAT daily cap is TZS 100,000, counted across all your collections on UAT. POST /api/v1/payment/card/intent answers 402 when it is reached.

Test cards​

Every card below was run against the UAT card gateway on 30 September 2026 and the result read back from the gateway. Use only these. Cards from Mastercard's or Visa's generic test lists mostly do not behave as published on this sandbox, because it runs on NMB's own acquirer link rather than Mastercard's emulator.

Cards that approve. Expiry 01/39, security code 100, amount TZS 1,000 or less.

Card numberBrandExpiryCSCAmounts verified to approveExpected result
5555555555000018Mastercard (credit)01/39100TZS 100, 500, 1,000Approved, captured
5506900140100305Mastercard (credit)01/39100TZS 500, 1,000Approved, captured
2222400070000005Mastercard01/39100TZS 1,000Approved, captured
5506900140100503Mastercard (credit)01/39100TZS 1,000Approved, captured
5500005555555559Mastercard (credit)01/39100TZS 1,000Approved, captured
5455031257390496Mastercard (credit)01/39100TZS 1,000Approved, captured
5455031252665454Mastercard (credit)01/39100TZS 1,000Approved, captured
5297410588409146Mastercard (debit)01/39100TZS 1,000Approved, captured
Amounts above TZS 1,000 are declined on UAT

The sandbox bank declines every amount above TZS 1,000 with "05 Do not honour", on every card, including TZS 1,001 and 1,000.5. Keep UAT card payments at TZS 1,000 or less when you want an approval, and use a bigger amount when you want a decline.

Cards that decline, and how. Expiry 01/39, security code 100, TZS 1,000.

Card numberBrandExpiryCSCGateway answergatewayCodeWhat the payer is told
5175540000050008Mastercard01/3910054 Expired cardEXPIRED_CARDThe card has expired.
5175540000050099Mastercard01/3910054 Expired cardEXPIRED_CARDThe card has expired.
5555555555554444Mastercard01/3910005 Do not honourDECLINEDThe card was declined by your bank.
5123450000000008Mastercard01/3910014 Invalid card numberDECLINEDThe card was declined by your bank.
5111111111111118Mastercard01/3910014 Invalid card numberDECLINEDThe card was declined by your bank.
4111111111111111Visa01/3910005 Do not honour (15 No such issuer at TZS 500 and 100)DECLINEDThe card was declined by your bank.
371881634498004American Express01/391000Refused before the bank: no acquirer relationship for this card networkERRORThis type of card is not accepted here. Try another card, or pay with mobile money.

What the sandbox does not do, so do not try to test it there:

  • No Visa card approves. Every Visa test card declines. American Express and Maestro are refused outright.
  • The expiry date is not checked. A past expiry such as 12/25 still approves. Use the two expired-card numbers above to test an expired card.
  • The security code is not checked. Any three digits approve. A wrong CSC cannot be tested on UAT.
  • Insufficient funds, timeouts and bank system errors cannot be produced.
  • 3D Secure is not available (see the rules above).

Scenario matrix​

Run these in order on UAT. Each one starts from a fresh card payment unless it says otherwise. Create it from your server:

curl -X POST https://core-uat.malipopay.co.tz/api/v1/payment/card/intent \
-H "apiToken: YOUR_UAT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000,
"currency": "TZS",
"description": "UAT card test",
"customerReference": "UAT-CARD-001"
}'

Then pay it either on the hosted checkout (open checkoutUrl, which is on https://demo.malipopay.co.tz) or through your own page with the element loaded from https://demo.malipopay.co.tz/embed/card.js. Card details are from the test card tables.

#ScenarioStepsExpected screenExpected API result
1ApprovedPayment of TZS 1,000. Pay with 5555555555000018, 01/39, 100No bank screen. The checkout confirms the payment on the same page; the element calls onPaidcomplete: data.status SUCCESSFUL, gatewayCode APPROVED, paymentStatus SUCCESSFUL, outstanding 0. Verify: status SUCCESSFUL, paidAmount 1000
2Declined by the bank (do not honour)Payment of TZS 1,500. Pay with 5555555555000018, 01/39, 100. Or TZS 1,000 with 5555555555554444"The card was declined by your bank." The card form stays, ready for another cardcomplete: data.status DECLINED, gatewayCode DECLINED. Verify: status PROCESSING, paidAmount 0
3Invalid card numberTZS 1,000. Pay with 5123450000000008"The card was declined by your bank."data.status DECLINED, gatewayCode DECLINED (the bank's code is 14)
4Expired cardTZS 1,000. Pay with 5175540000050008, 01/39, 100"The card has expired."data.status DECLINED, gatewayCode EXPIRED_CARD
5Wrong security codeNot testable on UAT: the sandbox never checks the CSC and any value approves. A missing CSC is refused (the gateway requires one)With the CSC left empty, the card form asks you to check the security codeOn production the issuer checks it; a refusal answers gatewayCode INVALID_CSC, "The security code was not accepted."
6Insufficient funds or do not honourInsufficient funds cannot be produced on UAT. Test "do not honour" with scenario 2As scenario 2On production, gatewayCode INSUFFICIENT_FUNDS, "The card was declined for insufficient funds."
73D Secure not supported (sandbox)Any approving cardNo bank or OTP screen ever appears. Payment completes on the pagecomplete: authenticationStatus AUTHENTICATION_NOT_SUPPORTED, then SUCCESSFUL. This is the expected sandbox behaviour, not a fault
8Card network not acceptedTZS 1,000. Pay with the American Express card 371881634498004, CSC 1000. Then a Visa card, for example 4111111111111111Amex: "This type of card is not accepted here. Try another card, or pay with mobile money." (never the gateway's raw text). Visa: "The card was declined by your bank."Amex: data.status DECLINED, gatewayCode ERROR. Visa: DECLINED / DECLINED
9Payer abandonsOpen the checkout or your page, type a card, close the tab without pressing Pay. Then open the same link againOn return the form is empty and ready. Nothing was chargedVerify: status PROCESSING, paidAmount 0. No webhook. The next prepare opens a new attempt (orderId ends in -C2)
10Duplicate submit and idempotencyAfter scenario 1 succeeds, call POST /api/v1/payment/card/complete/{reference} again with the same session. Also double-click Pay on a fresh paymentNothing new happens. One charge onlySecond complete: data.status SUCCESSFUL with alreadyPaid: true. Exactly one payment.confirmed webhook. prepare on the paid reference answers 400 This payment has already been paid
11Retry after a declineOne payment of TZS 1,000. Pay with 5123450000000008 (declines), then on the same page pay with 5555555555000018First: "The card was declined by your bank." Second: successSecond complete: SUCCESSFUL, orderId ends in -C2. Verify: paidAmount 1000, credited once. One webhook
12Part paymentOn a TZS 1,000 payment, call prepare with { "amount": 500 }Not offered on the checkout or the element while part payments are off400 Paying part of a payment by card is not available yet. Pay the outstanding TZS 1,000, or pay a part with mobile money. Part payments by card are off on UAT and production today
13Fraud block from rapid retriesPay four or five times in quick succession with the same card"This card could not be accepted for this payment. Try another card or pay with mobile money."data.status DECLINED, gatewayCode BLOCKED. Not a bank decline: the gateway's fraud engine refused the card. Other cards keep working; the same card worked again after about 8 minutes
14Webhook receivedRegister a webhook on your UAT project, then run scenario 1Nothing on screenYour endpoint receives payment.confirmed with status SUCCESSFUL, code 91000, transactionId ending in -C1-P, and an X-Malipopay-Signature that verifies. Scenarios 2 to 4 send no webhook
15Receipt names the cardAfter scenario 1, read GET /api/v1/payment/verify/{reference} and open the receiptThe receipt's payment method reads Card (Mastercard •••• 0018)Verify: method { "type": "card", "scheme": "MASTERCARD", "last4": "0018" }
16Card session refused for a payment that cannot be paidCall POST /api/v1/payment/card/initSession with no reference, then with the reference from scenario 1 (already paid), then with a payment the merchant cancelledNo card form opens (the element says "We could not start a secure card session.")400: A payment reference is required to start a card session, This payment has already been paid, This payment request was cancelled. Ask the business for a new one. No session is opened
17Payment cancelled while the payer is on the card formOpen the card form, then cancel the payment request from the dashboard, then press Pay"This payment request was cancelled. Ask the business for a new one."complete: data.status FAILED, gatewayCode PAYMENT_CANCELLED. The card is not charged. A new prepare answers 400 with the same sentence
18Capture unconfirmed (UNCONFIRMED)Cannot be triggered on purpose: it happens when the gateway does not answer the capture (a timeout). Recognise it when it happens"We are confirming this payment with your bank. Please do not pay again: this page will update."complete: data.status PENDING, gatewayCode UNCONFIRMED. What to do: poll GET /api/v1/payment/verify/{reference} every few seconds; verify asks the gateway and settles it. Never create a new payment or attempt for the order meanwhile; prepare refuses one ("still being confirmed with the bank")
19Capture never reached the gateway (NOT_SENT)Cannot be triggered on purpose. Recognise it when it happens"We could not reach the card gateway, and your card has not been charged. Please try again."complete: data.status DECLINED, gatewayCode NOT_SENT. The gateway confirmed it has no record, so nothing was charged and the payer may try again on the same reference
20Raw card details refusedSend POST /api/v1/payment with paymentMethodDetails.cardNumber (any value)Nothing on screen400, code CARD_DETAILS_NOT_ACCEPTED, details naming the fields to remove. Card details only ever go into the gateway-hosted fields

For every scenario, check three things, not one: what the payer saw, what GET /api/v1/payment/verify/{reference} says, and what your webhook endpoint received.

Live (production)​

On production there is no sandbox for cards. Every card payment on a production project is a real payment: the card is charged, the money moves, and it shows on the cardholder's statement. Test with a real card you own, for a small amount.

How to test on production​

  1. Use your production project and API key, and https://core-prod.malipopay.co.tz. Load the element from https://app.malipopay.co.tz/embed/card.js.
  2. Create a card payment for a small real amount, for example TZS 1,000. Your project's production limits apply; an account that is not yet go-live approved has the starter daily limit.
  3. Pay it with your own real Mastercard. Visa acceptance on live depends on NMB's acquiring setup and has not been verified yet; try a Visa card only after Malipopay confirms it is accepted, or expect it may be declined.
  4. Expect the 3D Secure challenge from your bank if the card is enrolled: an OTP by SMS, or an approval in your banking app. The page goes full screen to the bank and comes back on its own.
  5. Check the same things as on UAT: the receipt or your page shows success, payment.confirmed reached your webhook with a valid signature, and GET /api/v1/payment/verify/{reference} shows status: "SUCCESSFUL" with method.type: "card".
  6. Try one decline you can cause safely, for example a mistyped security code or an OTP you let expire, and confirm your page shows the message and lets the payer try again.

What only production can show​

  • The 3D Secure challenge and the return from it. After the challenge, a payment made on a Malipopay page lands straight on the receipt (/r/{slug}); a payment made through the element on your page comes back to that page with ?ref={reference}&card=successful.
  • Real issuer declines (insufficient funds, card blocked for online use, OTP failed), which the sandbox only approximates.

Refunds​

There is no card refund or void in Malipopay yet. No endpoint returns card money, and the platform sends no refund or reversal to the card gateway. A production test payment is therefore not undone automatically. Keep production test amounts small, and contact Malipopay support with the reference if a card payment must be returned.

Checklist before you go live with cards​

  • Every UAT scenario above passes, including the webhook and the duplicate checks.
  • Your server fulfils only on payment.confirmed (or on a server-side verify), never on the browser's word.
  • Your webhook handler is idempotent.
  • The element is mounted on the page the payer returns to after 3D Secure, with the same reference.
  • One small real card payment on production succeeded end to end, with the 3D Secure challenge.