paylod
Test in sandbox

Guides

Test M-Pesa payments in the Daraja sandbox

.md

Build the whole flow against Safaricom's sandbox with test credentials — including the failure paths — before a shilling moves.

Test every failure path without a phone

Start here. You do not need a handset to test a cancellation, a wrong M-Pesa PIN, insufficient funds or a timeout. Do not try to make these outcomes by hand. paylod supplies a simulator that uses the real settlement path. The simulator makes the same payment row, the same result codes, and the same signed webhook that your production code receives. Only the handset is simulated.

Two calls:

  1. Create a simulated payment — use paylod.simulate.collect() in the SDK, the simulate_test_payment MCP tool, or POST /simulate/collect over REST with your mp_test_… key. paylod sends no STK push. The call returns { paymentId, status: "pending", outcomes }, and outcomes is an array of { id, label, status }.
  2. Force the result — use paylod.simulate.outcome(), the simulate_outcome MCP tool, or POST /simulate/outcome. Give that paymentId and one outcome id.
outcomeSettles asResult code
approvesuccess0
user_cancelledfailed1032
timeoutfailed1037
wrong_pinfailed2001
insufficient_fundsfailed1

From the SDK

On Node, use this method. The method is one call. The method returns the same PaymentOutcome that your production code already displays. Thus you test the real code, and not special test code.

import { Paylod } from "@paylod/node";

const paylod = new Paylod(process.env.PAYLOD_TEST_KEY!);   // mp_test_…

for (const outcome of paylod.simulate.outcomes) {
  const result = await paylod.simulate.pay({
    outcome,
    amount: 10,
    idempotencyKey: `sim-${outcome}`,   // the simulator enforces the production rule
  });
  console.log(outcome, "→", result.status, result.retryable, result.message);
}
// approve            → succeeded false Payment received — thank you!
// wrong_pin          → failed    true  That M-Pesa PIN was incorrect. …
// insufficient_funds → failed    true  Your M-Pesa balance is too low. …
// user_cancelled     → cancelled true  Payment cancelled — you can try again …
// timeout            → failed    true  The M-Pesa prompt expired before it was answered. …

To test your handler, and not the handler of the SDK, make the client with { simulate: true }. collect() then creates a simulated payment, and sends no STK push. Your /api/pay route operates without a change. The SDK refuses an mp_live_… key locally, before it sends a request. The SDK reference gives the full details.

Over REST

Simulate a cancelled payment over REST
# 1. create the pending simulated payment
curl -sX POST https://paylod.dev/functions/v1/simulate/collect \
  -H "Authorization: Bearer $PAYLOD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"applicationId":"<your-app-id>","phone":"254708374149","amount":10}'
# → { "paymentId": "…", "status": "pending", "outcomes": [ { "id": "approve", … } ] }

# 2. force the outcome — your webhook fires, your poller settles, exactly as in production
curl -sX POST https://paylod.dev/functions/v1/simulate/outcome \
  -H "Authorization: Bearer $PAYLOD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paymentId":"<paymentId>","outcome":"user_cancelled"}'

An AI agent with the paylod:payments.simulate scope can do the same with simulate_test_payment and simulate_outcome. Such an agent cannot touch real money. See the MCP server.

[!note] The simulator operates only in sandbox. The simulator is the fastest method to prove three things: your code shows outcome.message, your code offers a retry only when retryable is true, and your code never fulfils an order on a 1032.

Get sandbox credentials

Create an app on the Safaricom Daraja portal. Then open the Sandbox credentials of the app. You need four values:

FieldSandbox value
Consumer keyFrom the Daraja portal
Consumer secretFrom the Daraja portal
Shortcode174379 (Safaricom's shared test paybill)
PasskeyFrom "Lipa na M-Pesa Online" in the portal

Copy the four values into the Sandbox environment of your paylod application. Then create an mp_test_… key.

What sandbox does

Sandbox is the environment of Safaricom, and not a paylod simulation. The STK push is real. The STK push goes to Daraja, Daraja answers, and the callback comes back to the hosted receiver of paylod. This is the same behaviour as production. Only the money is not real, because nothing settles.

Thus each item that you build is the same in production: the 202, the paymentId, the webhook envelope, the signature and the result codes. You do not write a second integration.

Send a test payment

Set PAYLOD_API_KEY to the mp_test_… key. Nothing else changes.

import { Paylod } from "@paylod/node";

const paylod = new Paylod(process.env.PAYLOD_API_KEY!);   // mp_test_… key

const outcome = await paylod.collectAndWait({
  amount: 10,
  phone: "254708374149",     // Safaricom's published test MSISDN
  accountReference: "TEST-1",
  idempotencyKey: attempt.id,
});

console.log(outcome.paid ? outcome.receipt : outcome.message);

Use a real number that you control, and you get a real prompt. No real money moves.

Test the failure paths

Most production incidents occur on a failure path that nobody tested. Before you go live, make sure that your code gives a correct result for each failure path below. Use the simulator above. The simulator is deterministic, needs no handset, and runs in your test suite. The handset column is only for a person who wants to see a real failure one time.

Result codeSimulator outcomeOn a real handsetWhat your code should do
1032user_cancelledPress Cancel on the promptOffer a retry. Do not fulfil the order
1037timeoutIgnore the prompt until it expiresRetry, or use the poll method
2001wrong_pinEnter the wrong M-Pesa PINAsk the customer to try again
1insufficient_fundsSend an amount above the test balanceTell the customer that the balance is low

The error reference gives every code with its cause and its correction.

Test your webhook locally

You need no tunnel. The CLI sends your live webhook events to localhost, and keeps the signature:

Terminal
npx @paylod/cli listen --forward http://localhost:3000/webhook

Your handler then verifies a real signature against a real payload, on your own computer. See Handle the result.

Next

Go live — replace the credentials, replace the key, and keep the code.