Skip to content

M-Pesa STK push result codes, and what to do about each one

What the ResultCode in a Daraja STK push callback means - 0, 1, 1032, 1037, 2001, 1001, 1019 - which failures are final, which are worth retrying, and how to tell a customer what happened.

· 7 min read · Noria Technologies

When you send an M-Pesa STK push through Safaricom’s Daraja API, the answer arrives later as a callback with a ResultCode and a ResultDesc. 0 means the customer paid. Everything else is a reason they did not, and the reasons are not all equal: some mean “ask again”, some mean “stop”, and one or two mean “you do not know yet”.

This guide covers the codes you will see in practice, what each one usually means for the customer standing at your till, and what your system should do next.

Where the result code comes from

An STK push has two answers, and it helps to keep them apart:

  1. The request response. Daraja answers your processrequest call straight away with a ResponseCode. 0 here only means Safaricom accepted the request and will try to prompt the phone. It does not mean anyone paid.
  2. The callback. Once the customer acts, or the prompt times out, Safaricom posts a callback to your CallBackURL with Body.stkCallback.ResultCode. This is the outcome.

A successful callback looks like this, trimmed:

{
  "Body": {
    "stkCallback": {
      "MerchantRequestID": "29115-34620561-1",
      "CheckoutRequestID": "ws_CO_191220191020363925",
      "ResultCode": 0,
      "ResultDesc": "The service request is processed successfully.",
      "CallbackMetadata": {
        "Item": [
          { "Name": "Amount", "Value": 2500 },
          { "Name": "MpesaReceiptNumber", "Value": "TKE4X7Q2LM" },
          { "Name": "PhoneNumber", "Value": 254712345678 }
        ]
      }
    }
  }
}

A failed callback has no CallbackMetadata, only the code and description.

The codes you will actually see

ResultCode Typical ResultDesc What happened Final?
0 The service request is processed successfully. The customer entered their PIN and the payment went through. Yes, paid
1 The balance is insufficient for the transaction. Not enough money in the M-Pesa account, including Fuliza limits. Yes
1032 Request cancelled by user. The customer pressed Cancel, or dismissed the prompt. Yes
1037 DS timeout user cannot be reached. The phone was off, out of coverage, or the prompt was never answered in time. Yes
2001 The initiator information is invalid. Most often a wrong M-Pesa PIN. Yes
1001 Unable to lock subscriber, a transaction is already in process for the current subscriber. The customer already has a prompt or transaction in progress. Yes, retry later
1019 Transaction has expired. The request was not completed before it expired. Yes
1025, 9999 An error occurred while sending a push request. Safaricom could not send the prompt, often because a field such as the account reference is too long or malformed. Yes, fix the request

The descriptions vary slightly between sandbox and production and change from time to time, so match on ResultCode, never on the text.

What to do about each one

0 - paid. Before you release goods, check the amount and record the MpesaReceiptNumber. Treat the receipt as unique: if the same receipt arrives twice, the second one is a duplicate, not a second payment.

1032 and 1037 - the customer did not finish. These are the most common failures and the most recoverable. The customer is usually still willing to pay. Offer to send the prompt again, and say plainly what happened: “You cancelled the M-Pesa prompt” reads very differently from “Payment failed”.

1 - insufficient balance. Tell the customer, and do not retry automatically. Sending the same prompt again will fail the same way and annoys people.

2001 - wrong PIN. Let the customer try again. Repeated wrong PINs can lock their M-Pesa account, so do not loop.

1001 - another transaction in progress. Wait a short while before sending another prompt to the same number. Prompting again immediately usually returns the same code.

1025 and 9999 - the prompt never left Safaricom. This is almost always your request. Check the phone number format (2547XXXXXXXX or 2541XXXXXXXX), keep AccountReference to 12 characters and TransactionDesc short, and make sure the amount is a whole number of shillings.

The case with no result code at all

Sometimes no callback arrives. The customer may have paid, or may not. This is not a failure, it is an unknown, and treating it as a failure is how businesses end up charging twice.

The right move is to ask Safaricom using the STK push query API with the CheckoutRequestID, and to keep the original charge open until you get a definite answer. Never send a fresh prompt to cover a payment whose outcome you do not know. The guide on Daraja callbacks that never arrive covers this in detail.

How Noria Pay handles result codes

Noria Pay runs Daraja on your own shortcode and turns these callbacks into one normalised status:

  • A 0 is confirmed with Safaricom before the payment is marked succeeded, and the receipt is stored under a unique index, so duplicate callbacks settle a payment once.
  • Any other code marks the payment failed and keeps Safaricom’s ResultDesc as the failure reason, so your webhook and dashboard show why.
  • If no callback arrives within a grace period, Pay queries the transaction itself and applies Safaricom’s answer.

Your systems receive one signed webhook per change, with status, failure_reason and provider_receipt, whichever code Safaricom sent:

{
  "type": "failed",
  "data": {
    "status": "failed",
    "provider": "daraja",
    "reference": "INV-1042",
    "failure_reason": "Request cancelled by user."
  }
}

A short checklist

  • Match on ResultCode, not ResultDesc.
  • Treat the request response and the callback as two different answers.
  • Store MpesaReceiptNumber as unique.
  • Offer a fresh prompt after 1032 and 1037; do not auto-retry 1 or 2001.
  • When there is no callback, query. Do not send a second prompt.

Ready to take payments properly?

Tell us what you are building and which providers you use. We set up your workspace and help you go live.