Test Scenarios by Payment Method¶
Each scenario below is the same four steps with a different destination. Work through the happy path first, then the four failures — those are the ones that decide whether your integration survives contact with production.
All of them need sandbox credentials and your sandbox wallet's account number: see Sandbox Credentials & Test Numbers.
1. The happy path: C2B M-Pesa collection¶
The same flow as the Quickstart, laid out as a repeatable scenario.
- Authenticate —
GET /auth/token/with your sandbox client ID and secret. See Authentication. - Initiate —
POST /payments/initiate/withtransaction_type: "C2B",payment_method: "mpesa",account_number: "254700000000"and your sandbox wallet asmerchant_id. See Initiate Payment. - Poll status —
POST /payments/status-query/with yourmerchant_referenceuntilstatusis final. See Transaction Status Query. - Confirm in statement —
GET /transactions/?account_number=<sandbox_merchant_id>and confirm the transaction appears. See Transactions Statement.
The result arrives a few seconds after step 2, so expect the first poll to still show the payment in flight. That is not a failure — it is what a real STK prompt looks like while the customer is still deciding.
Repeat this shape for B2C payouts, B2B transfers and the bank, card, airtel and tkash methods.
The outcome table works the same way for all of them.
2. Customer cancels — 254700000001¶
Same request, different destination. The initiate call still succeeds: the customer has not decided yet at that point.
Then the result arrives with ResultCode 1032 and the payment goes to failed.
What this tests: that you do not treat a successful initiate response as a completed payment. If your code releases goods at step 2, this scenario ships them for free.
3. Insufficient funds — 254700000002¶
As above, with ResultCode 1.
What this tests: that your failure handling distinguishes "the customer could not pay" from "something broke". These are the same HTTP status to you and very different things to your support team.
4. Timeout — 254700000003¶
The initiate call succeeds. Then nothing arrives, ever. No callback, no status change.
Walk this one through deliberately:
- Initiate to
254700000003. - Wait past whatever timeout your own code uses.
- Call
POST /payments/status-query/. It answers — the payment is still processing. - Decide what your system does now.
What this tests: the case that produces most support tickets in production. Your integration needs an answer to "we never heard back" that is neither "assume it failed" (the customer may have paid) nor "wait forever" (the order never resolves). Status query is that answer.
5. Ambiguous payout — 254700000004¶
Use transaction_type: "B2C" and destination 254700000004. This time the initiate call itself fails.
The payout is recorded as reconciliation_required. Hivemind Payments does not retry it against another provider,
because we do not know whether the first one already moved the money — retrying is how a recipient gets
paid twice. A human settles it.
What this tests: that your code does not automatically re-send a failed payout. If it does, this is the scenario where that becomes a duplicate payment in production. Surface it as "pending investigation" and stop.
6. Late result — 254700000005¶
Initiate, then wait. The successful result arrives about two minutes later.
What this tests: that a slow result does not get overwritten by a premature decision. If your polling loop gives up after 30 seconds and marks the payment failed, your records now disagree with ours about a payment that succeeded — and the customer has been charged.
Testing webhook delivery alongside polling¶
Set result_url on your initiate calls to a webhook.site URL or an ngrok tunnel,
and watch the callbacks arrive while you poll. Every scenario above except the timeout delivers one.
Receiving a sandbox result on your webhook and returning a 2xx is also one of the go-live requirements — so this step is not optional housekeeping, it is part of the path to production. See Verifying & Testing Webhooks.