paylod
Error codes

Reference

M-Pesa and Daraja error codes, decoded

.md

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:

FieldWhere it comes from
HTTP statuspaylod. 401 wrong API key, 422 wrong body, 502 Daraja refused the request. See API errors.
resultCode / resultDescM-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.

decode.ts
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

CodeMeaningWhat to do
0Success.Fulfil the order, but first check the settled amount.
1032The customer pressed Cancel on the prompt.Offer the customer a retry. This code is not an error in your code.
1037Timeout. 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.
2001Wrong M-Pesa PIN.Ask the customer to try again.
1Insufficient balance.Tell the customer that the M-Pesa balance is too low.
4999Not 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 retryable

Success

The STK Push transaction completed and the customer's payment was received successfully.

Fix

Treat the transaction as paid, read the receipt from CallbackMetadata, and fulfil the order.

Open error page
1STK resultBalanceRetryable

Insufficient M-Pesa balance

The customer does not have enough M-Pesa balance (including any available Fuliza overdraft) to cover the amount.

Fix

Nothing to fix on your side — ask the customer to top up their M-Pesa and try the payment again.

Open error page
17STK resultM-Pesa systemNot retryable

M-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.

Fix

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.

Open error page
26STK resultM-Pesa systemNot retryable

M-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.

Fix

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.

Open error page
1001STK resultCustomerNot retryable

A 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.

Fix

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.

Open error page
1019STK resultCustomerRetryable

Transaction 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.

Fix

Let the customer re-initiate the payment and prompt them to approve promptly.

Open error page
1025STK resultM-Pesa systemNot retryable

Error 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.

Fix

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.

Open error page
9999STK resultM-Pesa systemNot retryable

Error 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.

Fix

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.

Open error page
1032STK resultCustomerRetryable

Payment 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.

Fix

Nothing is wrong with your setup — offer a clear retry button so the customer can try again.

Open error page
1037STK resultCustomerRetryable

The 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.

Fix

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.

Open error page
2001STK resultCustomerRetryable

Wrong 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.)

Fix

Nothing to change on your side — ask the customer to retry and enter their correct M-Pesa PIN.

Open error page
2028STK resultLimitNot retryable

Payment 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).

Fix

Ask the customer to pay a smaller amount, or split the payment. Confirm the amount is within Safaricom's transaction limits.

Open error page
2029STK resultYour setupNot retryable

Buy 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.

Fix

Match the transaction type to the shortcode: CustomerBuyGoodsOnline + till number for a till, CustomerPayBillOnline + paybill for a paybill. Fix it in Credentials and retry.

Open error page
4999STK resultStill waitingNot retryable

Still 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.

Fix

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.

Open error page
500.001.1001STK resultStill waitingNot retryable

Transaction 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.)

Fix

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.

Open error page
400.002.02API errorYour setupNot retryable

Bad 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.

Fix

Send the body as JSON, and verify the amount (a positive whole-number KES) and BusinessShortCode are valid before sending.

Open error page
400.002.05API errorYour setupNot retryable

Invalid request payload

The request payload is structurally invalid or missing required fields, so Daraja returns HTTP 400 before processing.

Fix

Validate the JSON schema and required parameters against the endpoint spec before sending.

Open error page
400.008.02API errorYour setupNot retryable

Invalid phone number

The PhoneNumber/PartyA value is not a valid Safaricom MSISDN in the required 2547XXXXXXXX international format.

Fix

Normalize the phone number to the 12-digit 2547XXXXXXXX format before sending.

Open error page
401.002.01API errorYour setupRetryable

Unauthorized — invalid or expired access token

The OAuth access token is missing, malformed, or expired (tokens have a ~1 hour TTL), returning HTTP 401.

Fix

Request a fresh token and send it as an 'Authorization: Bearer <token>' header, refreshing before expiry.

Open error page
404.001.03API errorYour setupRetryable

Invalid 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.

Fix

Confirm the token is valid and current, and contact Safaricom to enable/whitelist the API on your production shortcode.

Open error page
404.001.04API errorYour setupNot retryable

Invalid authentication header

The Authorization header is missing or incorrectly formatted, so the request cannot be authenticated (HTTP 404).

Fix

Ensure the header is exactly 'Authorization: Bearer <access_token>' with no extra whitespace.

Open error page
Bad Request - Invalid Initiator InformationAPI errorYour setupNot retryable

Invalid 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.

Fix

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.

Open error page
500.001.1001API errorM-Pesa systemNot retryable

Server 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.

Fix

Verify the shortcode/merchant configuration and business account balance, then retry or contact Safaricom support.

Open error page
500.002.1001API errorM-Pesa systemRetryable

Service temporarily unavailable / under maintenance

The Daraja service is temporarily unavailable, throttled (spike arrest), or under maintenance, returning HTTP 500.

Fix

Implement exponential backoff and retry later; if persistent, check the Safaricom status page or support.

Open error page
0C2B / B2CSuccessNot retryable

Success / Accepted

The C2B/B2C/Reversal/Transaction Status/Account Balance request was processed successfully (for C2B validation this is the 'Accept' response).

Fix

Return ResultCode 0 to accept, and process the result/receipt details from the confirmation callback.

Open error page
2001C2B / B2CYour setupNot retryable

Invalid 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.)

Fix

Re-encrypt the initiator password with Safaricom's RSA public cert, confirm the initiator belongs to the shortcode, and renew before expiry.

Open error page
C2B00011C2B / B2CCustomerNot retryable

Invalid MSISDN (Reject)

During C2B validation the payer's phone number (MSISDN) is considered invalid by your validation logic.

Fix

Return this reject code when the MSISDN is unrecognized, or accept if you do not need to validate the payer number.

Open error page
C2B00012C2B / B2CCustomerNot retryable

Invalid Account Number (Reject)

During C2B validation the account number / bill reference supplied by the payer does not exist in your system.

Fix

Return this reject code for unknown account references, or map/create the account before accepting.

Open error page
C2B00013C2B / B2CCustomerNot retryable

Invalid Amount (Reject)

During C2B validation the paid amount fails your rules (for example the wrong amount for the invoice).

Fix

Return this reject code when the amount is invalid, or relax the amount check if partial payments are allowed.

Open error page
C2B00014C2B / B2CCustomerNot retryable

Invalid KYC Details (Reject)

During C2B validation the payer's KYC details do not meet your requirements.

Fix

Return this reject code when KYC checks fail, otherwise accept the transaction.

Open error page
C2B00015C2B / B2CYour setupNot retryable

Invalid Shortcode (Reject)

During C2B validation the shortcode targeted by the payment is considered invalid.

Fix

Return this reject code for an unexpected shortcode, or verify your shortcode configuration.

Open error page
C2B00016C2B / B2CCustomerNot retryable

Other Error (Reject)

A catch-all C2B validation rejection for any other reason not covered by the specific reject codes.

Fix

Return this reject code for unclassified validation failures, and log the reason for follow-up.

Open error page