Reference
M-Pesa and Daraja error codes, decoded
Every Safaricom M-Pesa result code and Daraja API error decoded — cause, fix, and a customer-facing message. Searchable.
M-Pesa reports a problem as a number and gives almost no other information. resultCode: 1037 is an example. That number means nothing until you decode it.
This page decodes every result code. For each code it gives the meaning, the cause, whether a retry can help, and the message for the customer.
How to read a failure
A failed payment gives you two fields. The two fields come from two different places:
| Field | Where it comes from |
|---|---|
| HTTP status | paylod. 401 wrong API key, 422 wrong body, 502 Daraja refused the request. See API errors. |
resultCode / resultDesc | M-Pesa. The customer cancelled the prompt, or the PIN was wrong, or the handset was off. This page decodes these codes below. |
A 202 and then resultCode: 1032 is not a bug. The push worked, and the customer pressed Cancel.
You do not have to implement this table
The Node SDK decodes result codes offline. It makes no network call. It needs no API key at call time.
import { decodeError } from "@paylod/node";
const err = decodeError(1032);
err.title; // "Payment cancelled by the customer"
err.category; // "customer"
err.retryable; // true
err.customerMessage; // "Payment cancelled — you can try again whenever you're ready."On a failed payment, collectAndWait() returns the same object as outcome.detail. A payment.failed webhook carries the same object as event.data.decoded. The two objects are byte-for-byte identical. The message that you show to a customer is therefore the same for every integration. The SDK also exports the whole catalogue as ERROR_CATALOG.
Most integrations do not need the raw catalogue. outcome.message is the decoded sentence for the customer. outcome.retryable tells you whether a second charge is safe. Use the catalogue below for your records, for your dashboards, and for debug work during an incident.
The CLI decodes the same catalogue in your terminal. Run paylod errors 1032. This command is the fastest way to read the meaning of a code during an incident.
Do not show a failure for the codes `4999` and `500.001.1001`. These two codes mean pending, not failed. Daraja returns them while the customer still looks at the PIN prompt. They decode with category: "pending" and retryable: false. Here retryable: false does not mean that a retry is pointless. It means that the first prompt is still live, and a second push can double-charge the customer. Do not collect again on these two codes. Continue to poll the status. The SDK polls for you.
The most common codes
| Code | Meaning | What to do |
|---|---|---|
0 | Success. | Fulfil the order, but first check the settled amount. |
1032 | The customer pressed Cancel on the prompt. | Offer the customer a retry. This code is not an error in your code. |
1037 | Timeout. The prompt did not reach the handset, or the customer ignored it. | Retry. The handset was off, or out of coverage, or the customer did not answer. |
2001 | Wrong M-Pesa PIN. | Ask the customer to try again. |
1 | Insufficient balance. | Tell the customer that the M-Pesa balance is too low. |
4999 | Not a failure. The customer did not enter the PIN yet. | Continue to poll. Never collect again: the prompt is still live, and a second push can double-charge the customer. |
The catalogue below contains every other code. It contains every Daraja API error, every C2B result code and every B2C result code. You can search it.
Full catalogue
Error codes
Search Safaricom's M-Pesa result and error codes by number or description. Every failed payment in Payments is decoded with the same reference.
32 codes
0STK resultSuccessNot retryableSuccess
The STK Push transaction completed and the customer's payment was received successfully.
Treat the transaction as paid, read the receipt from CallbackMetadata, and fulfil the order.
1STK resultBalanceRetryableInsufficient M-Pesa balance
The customer does not have enough M-Pesa balance (including any available Fuliza overdraft) to cover the amount.
Nothing to fix on your side — ask the customer to top up their M-Pesa and try the payment again.
17STK resultM-Pesa systemNot retryableM-Pesa system internal error
Safaricom's system (or the receiving shortcode / Party B) was temporarily unable to process the transaction — a transient M-Pesa-side error, not a problem with your request.
This is transient, but it is NOT proven that no charge was raised. Confirm the payment's final state with GET /status/:id (or the webhook) before charging again; only start a new attempt with a NEW idempotency key once you have confirmed nothing moved.
26STK resultM-Pesa systemNot retryableM-Pesa system busy
M-Pesa is under high load and rejected the request because the system was busy. This is an M-Pesa-side condition, not your configuration.
Back off, then confirm the payment's final state with GET /status/:id (or the webhook) before charging again — a busy-system rejection is not proof no charge was raised. Only start a new attempt with a NEW idempotency key once you have confirmed nothing moved.
1001STK resultCustomerNot retryableA transaction is already in process for this number
M-Pesa could not lock the subscriber because the customer's line already has an active USSD/M-Pesa session or an in-flight transaction, so a new STK push cannot start.
Ask the customer to finish or dismiss any open M-Pesa/USSD prompt, then wait 1-3 minutes. Do NOT blind-retry: the in-flight transaction may be your own earlier push, and charging again could double-charge. Confirm the outcome of the existing payment first.
1019STK resultCustomerRetryableTransaction expired
The transaction exceeded its allowed processing window and M-Pesa expired it. Daraja says only 'Transaction expired.' and does not say why, so we do not guess — the usual reason is the customer taking too long to act on the prompt. Terminal: no money moved.
Let the customer re-initiate the payment and prompt them to approve promptly.
1025STK resultM-Pesa systemNot retryableError sending the STK prompt
M-Pesa could not send the STK prompt — usually a transient system error, or the request message (TransactionDesc) exceeded the 182-character limit.
Keep TransactionDesc within 182 characters. Before charging again, confirm the payment's final state with GET /status/:id (or the webhook) — a send failure is not proof no charge was raised. Only start a new attempt with a NEW idempotency key once you have confirmed nothing moved.
9999STK resultM-Pesa systemNot retryableError sending the STK prompt
The push could not be delivered — commonly a transient M-Pesa error, or the request message exceeds the 182-character limit.
Shorten the request fields (especially TransactionDesc) to stay within 182 characters. Before charging again, confirm the payment's final state with GET /status/:id (or the webhook) — a delivery failure is not proof no charge was raised. Only start a new attempt with a NEW idempotency key once you have confirmed nothing moved.
1032STK resultCustomerRetryablePayment cancelled by the customer
The customer received the STK prompt on their phone but pressed Cancel instead of entering their M-Pesa PIN. No money moved.
Nothing is wrong with your setup — offer a clear retry button so the customer can try again.
1037STK resultCustomerRetryableThe M-Pesa prompt went unanswered
M-Pesa sent the STK prompt but received no answer before it expired (~60s). Daraja reports this as 'DS timeout user cannot be reached', and has been OBSERVED live returning ResultDesc 'No response from user.' — the code covers BOTH the customer simply not acting on the prompt (the common case: they ignored, missed or dismissed it) AND a genuinely unreachable handset (phone off, out of coverage). Daraja does not tell us which, so neither do we. Terminal: no money moved.
Nothing is wrong with your setup. Offer a retry and ask the customer to keep their phone to hand and enter their PIN when the prompt appears. Only if the SAME number fails this way repeatedly is it worth checking that the line is a reachable Safaricom number.
2001STK resultCustomerRetryableWrong M-Pesa PIN
The customer entered the wrong M-Pesa PIN when approving the STK prompt. This is a customer input problem, NOT an issue with your credentials or configuration. NOTE Daraja words this confusingly: on an STK callback it sends ResultDesc 'The initiator information is invalid.' — that wording is about the CUSTOMER's PIN, not your initiator credentials. (The same numeric code on a B2C/C2B result genuinely IS an initiator-credential failure; that is a separate entry in this table, disambiguated by family.)
Nothing to change on your side — ask the customer to retry and enter their correct M-Pesa PIN.
2028STK resultLimitNot retryablePayment amount exceeds the M-Pesa limit
The requested amount is above the customer's allowed M-Pesa transaction or wallet limit (per-transaction or daily maximum).
Ask the customer to pay a smaller amount, or split the payment. Confirm the amount is within Safaricom's transaction limits.
2029STK resultYour setupNot retryableBuy Goods till sent as a Paybill request (or vice versa)
The shortcode is a Buy Goods till but the STK request used CustomerPayBillOnline (or a Paybill was sent as CustomerBuyGoodsOnline). Daraja rejects the transaction-type / shortcode mismatch.
Match the transaction type to the shortcode: CustomerBuyGoodsOnline + till number for a till, CustomerPayBillOnline + paybill for a paybill. Fix it in Credentials and retry.
4999STK resultStill waitingNot retryableStill waiting for the customer's PIN
The STK prompt is live on the customer's phone and they have not entered their M-Pesa PIN yet. This is NOT a failure — the payment is still in flight and can still succeed. Retrying now would push a SECOND prompt and can double-charge the customer.
Keep polling GET /status/:id (or wait for the webhook). Do NOT retry the charge and do NOT tell the customer it failed — this payment is still live and can still succeed.
500.001.1001STK resultStill waitingNot retryableTransaction is still being processed
M-Pesa is still processing this STK Push — the customer may not have entered their PIN yet. This is NOT a failure. (Daraja overloads this code: on a SYNCHRONOUS API call it can instead mean 'merchant does not exist' / 'wrong credentials' — that terminal case is matched on the message, see the api_error entry for the same code.)
Keep polling GET /status/:id (or wait for the webhook). Do NOT retry the charge and do NOT tell the customer it failed — this payment is still live and can still succeed.
400.002.02API errorYour setupNot retryableBad Request — invalid amount or shortcode
The synchronous request was rejected as malformed, commonly due to an invalid amount, an invalid BusinessShortCode, or the payload being sent as form data instead of JSON.
Send the body as JSON, and verify the amount (a positive whole-number KES) and BusinessShortCode are valid before sending.
400.002.05API errorYour setupNot retryableInvalid request payload
The request payload is structurally invalid or missing required fields, so Daraja returns HTTP 400 before processing.
Validate the JSON schema and required parameters against the endpoint spec before sending.
400.008.02API errorYour setupNot retryableInvalid phone number
The PhoneNumber/PartyA value is not a valid Safaricom MSISDN in the required 2547XXXXXXXX international format.
Normalize the phone number to the 12-digit 2547XXXXXXXX format before sending.
401.002.01API errorYour setupRetryableUnauthorized — invalid or expired access token
The OAuth access token is missing, malformed, or expired (tokens have a ~1 hour TTL), returning HTTP 401.
Request a fresh token and send it as an 'Authorization: Bearer <token>' header, refreshing before expiry.
404.001.03API errorYour setupRetryableInvalid access token
Daraja reports the access token as invalid on HTTP 404; often the token is fine but the shortcode is not authorized/whitelisted for that specific API.
Confirm the token is valid and current, and contact Safaricom to enable/whitelist the API on your production shortcode.
404.001.04API errorYour setupNot retryableInvalid authentication header
The Authorization header is missing or incorrectly formatted, so the request cannot be authenticated (HTTP 404).
Ensure the header is exactly 'Authorization: Bearer <access_token>' with no extra whitespace.
Bad Request - Invalid Initiator InformationAPI errorYour setupNot retryableInvalid initiator information
For secured APIs (B2C, Reversal, Transaction Status, Account Balance), the initiator username or the RSA-encrypted SecurityCredential is wrong, does not belong to the shortcode, or has expired.
Re-encrypt the initiator password with Safaricom's current RSA public certificate, confirm the initiator name belongs to the shortcode, and re-save it in Credentials.
500.001.1001API errorM-Pesa systemNot retryableServer error — merchant does not exist or insufficient funds
A server-side error (HTTP 500), commonly a nonexistent merchant/shortcode or, for B2C, insufficient funds in the business account.
Verify the shortcode/merchant configuration and business account balance, then retry or contact Safaricom support.
500.002.1001API errorM-Pesa systemRetryableService temporarily unavailable / under maintenance
The Daraja service is temporarily unavailable, throttled (spike arrest), or under maintenance, returning HTTP 500.
Implement exponential backoff and retry later; if persistent, check the Safaricom status page or support.
0C2B / B2CSuccessNot retryableSuccess / Accepted
The C2B/B2C/Reversal/Transaction Status/Account Balance request was processed successfully (for C2B validation this is the 'Accept' response).
Return ResultCode 0 to accept, and process the result/receipt details from the confirmation callback.
2001C2B / B2CYour setupNot retryableInvalid initiator information
For B2C and other secured APIs, the initiator username / RSA-encrypted SecurityCredential does not match the shortcode or has expired. (Note: on an STK Push callback, ResultCode 2001 means the customer entered the wrong PIN — see the STK result entry.)
Re-encrypt the initiator password with Safaricom's RSA public cert, confirm the initiator belongs to the shortcode, and renew before expiry.
C2B00011C2B / B2CCustomerNot retryableInvalid MSISDN (Reject)
During C2B validation the payer's phone number (MSISDN) is considered invalid by your validation logic.
Return this reject code when the MSISDN is unrecognized, or accept if you do not need to validate the payer number.
C2B00012C2B / B2CCustomerNot retryableInvalid Account Number (Reject)
During C2B validation the account number / bill reference supplied by the payer does not exist in your system.
Return this reject code for unknown account references, or map/create the account before accepting.
C2B00013C2B / B2CCustomerNot retryableInvalid Amount (Reject)
During C2B validation the paid amount fails your rules (for example the wrong amount for the invoice).
Return this reject code when the amount is invalid, or relax the amount check if partial payments are allowed.
C2B00014C2B / B2CCustomerNot retryableInvalid KYC Details (Reject)
During C2B validation the payer's KYC details do not meet your requirements.
Return this reject code when KYC checks fail, otherwise accept the transaction.
C2B00015C2B / B2CYour setupNot retryableInvalid Shortcode (Reject)
During C2B validation the shortcode targeted by the payment is considered invalid.
Return this reject code for an unexpected shortcode, or verify your shortcode configuration.
C2B00016C2B / B2CCustomerNot retryableOther Error (Reject)
A catch-all C2B validation rejection for any other reason not covered by the specific reject codes.
Return this reject code for unclassified validation failures, and log the reason for follow-up.