Daraja Error 1032 'Request Cancelled by User' Is Not a Bug in Your M-Pesa Integration
It is 9pm and the on-call phone buzzes. The alert says PAYMENT_ERROR, severity high, and the count is climbing. Someone opens the dashboard, someone else starts reading logs, and a third person is already typing "is Daraja down?" into the group chat. Ten minutes in, the truth arrives: the error is 1032. A customer opened the STK prompt on their phone, thought about it, and pressed Cancel.
Nothing is broken. There is no bug to fix, no incident to write up, no backend at fault. You paged three engineers because a person changed their mind about a payment.
Daraja error 1032 is not a system error. It is a user action, and the moment your code treats it as a failure, you have built an alerting system that fires every time a customer says "no." Here is how to handle it properly, and how to tell it apart from the codes that genuinely mean something went wrong.
1032 in an M-Pesa STK Push callback means the user pressed Cancel on the prompt, or let it sit until it auto-dismissed. It is a normal, expected outcome, not an application error.
- Do not alert or page on
1032. Log it at info level and move on. - In your UI, show a calm "Payment cancelled" message and offer a retry button that starts a fresh STK push.
- Track it separately in analytics as a user-cancellation, not lumped in with real failures.
What error 1032 looks like in the callback
You do not get 1032 from the initial STK push request; that request returns a CheckoutRequestID and a "Success. Request accepted for processing" acknowledgement. The 1032 arrives later, asynchronously, on your callback URL once the customer has interacted with the prompt:
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 1032,
"ResultDesc": "Request cancelled by user"
}
}
}
Two things to notice. First, there is no CallbackMetadata block. On a successful payment (ResultCode 0) Daraja sends back the amount, the receipt number, and the phone number inside CallbackMetadata. On 1032 there is nothing to send, because no money moved. Second, the ResultDesc is human-readable and says exactly what happened: the request was cancelled by the user. The code label you will see in Daraja's documentation is "Request Cancelled by User."
Why 1032 happens, and why it is not your problem to fix
An STK Push is a conversation with a human holding a phone. Your server asks Safaricom to send a prompt; Safaricom pushes it to the SIM; the prompt appears over whatever the customer was doing. From that moment, the outcome is entirely in the customer's hands, and there are only a few ways it can end.
The customer can enter their PIN, and the payment goes through with ResultCode 0. They can press Cancel, which is 1032. Or they can ignore the prompt until it times out, which is a different code. In every branch except the first, no money moved, and in none of them did your server, your integration, or Safaricom's platform malfunction. 1032 is the API faithfully reporting a human decision.
This matters because the instinct to "handle every non-zero ResultCode as an error" is exactly what produces the false page. A non-zero code is not a failure; it is an outcome. Some outcomes are failures, some are the customer exercising the Cancel button you asked Safaricom to show them.
Handle it correctly in three steps
Step 1: Branch on the ResultCode, do not treat non-zero as failure
In your callback handler, separate the three meanings: success, user-driven non-payment, and genuine error. Only the third deserves an alert.
def handle_stk_callback(payload):
cb = payload["Body"]["stkCallback"]
code = cb["ResultCode"]
checkout_id = cb["CheckoutRequestID"]
if code == 0:
confirm_payment(checkout_id, cb["CallbackMetadata"])
elif code in (1032, 1037, 1019, 1031):
# User-driven or non-error terminations. Record, do not alert.
mark_not_completed(checkout_id, reason=code)
else:
# Genuine problems worth a human looking.
log_payment_error(checkout_id, code, cb.get("ResultDesc"))
return {"ResultCode": 0, "ResultDesc": "Accepted"}
Note the handler always acknowledges Daraja with its own {"ResultCode": 0} so Safaricom does not retry the callback. That acknowledgement is unrelated to the payment's outcome; it just means "I received your callback." Getting these two zeros confused is a separate class of bug covered in why your M-Pesa callback fires twice.
Step 2: Tell the customer the truth, then offer a retry
On the front end, a cancelled payment is not an error state with a red banner. It is a neutral message and a clear way forward:
Payment cancelled. No money was deducted.
[ Try again ] [ Choose another method ]
When they tap "Try again," start a completely fresh STK push with a new CheckoutRequestID. Do not attempt to reuse or resume the cancelled one; that request is finished. If a customer retries too quickly you may hit error 1001, transaction in progress, so give the previous request a moment to clear before firing a new one.
Step 3: Keep 1032 out of your error metrics
Record cancellations, but in their own bucket. A high cancellation rate is a product signal (confusing checkout, wrong amount shown, slow prompt), not an engineering incident. If 1032 sits in the same counter as timeouts and platform faults, you can never see either clearly. Give it its own dimension in analytics and leave your alerting thresholds for the codes that mean something is actually wrong.
Verify your handling is right
You can reproduce 1032 on demand, which makes this easy to test end to end:
1. Trigger an STK push to a real phone (sandbox test MSISDN or your own line).
2. When the prompt appears, press Cancel instead of entering the PIN.
3. Confirm your callback receives ResultCode 1032 with no CallbackMetadata.
4. Confirm: no alert fired, the UI shows "Payment cancelled", the retry
button starts a fresh push, and your analytics logged a cancellation
(not an error).
If all four hold, your integration treats a human saying "no" as a human saying "no." For the full request-to-callback flow around this, see the Daraja STK Push, C2B and B2C production guide.
Do not confuse 1032 with the codes that look similar
The reason teams mishandle 1032 is that several codes all mean "the payment did not complete," and they get flattened into one "failed" bucket. They have different causes and deserve different responses.
| ResultCode | Meaning | Cause | Right response |
|---|---|---|---|
0 | Success | PIN entered, money moved | Fulfil the order |
1032 | Request cancelled by user | Customer pressed Cancel or let it dismiss | Info log, offer retry, no alert |
1037 | Timeout / user unreachable | Prompt never got a response, or the phone was unreachable | Offer retry, no alert |
1019 | Transaction expired | The request aged out before completion | Start a fresh push |
1001 | Transaction in progress | Another push for the same number is still pending | Wait, then retry |
The key line to draw is between 1032 (the user actively cancelled) and 1037 (the request never got a decision at all). Both end without payment, but they mean different things about the customer's intent, and if you want to know whether your checkout is confusing or your prompts are not arriving, you need them separated. The wider lesson: not every non-zero Daraja ResultCode is a failure, and building your error handling as if it were guarantees noise.
When it is still noisy
- If 1032 is genuinely high, look at the product, not the code. Is the amount on the prompt what the customer expected? Is the STK push arriving seconds after they clicked pay, or long enough that they forgot? Slow or surprising prompts get cancelled.
- If you are paging on it, fix the alert rule, not the payment. Exclude
1032,1037and1019from anything that wakes a human. - If cancellations correlate with one device or region, you may be seeing prompt-delivery delays rather than real cancellations misreported. Compare
1032and1037rates over time to tell them apart. - If you cannot tell success from cancellation at all, your callback handler is probably keying only on "did we get a callback" rather than the
ResultCode. Fix the branching first.
The whole point: an M-Pesa integration that pages you when a customer presses Cancel is not a robust integration, it is a fragile one that has confused user behaviour with system failure. Model the human decisions as first-class outcomes, reserve your alerts for the platform actually misbehaving, and the 9pm phone stays quiet.
Frequently asked questions
- What does M-Pesa ResultCode 1032 mean?
- It means the customer cancelled the STK Push prompt, either by pressing Cancel or by letting it sit until it auto-dismissed. It arrives on your callback URL with the ResultDesc 'Request cancelled by user' and no CallbackMetadata, because no money moved. It is a normal user action, not a system error or a bug in your integration.
- Should I alert or page on Daraja error 1032?
- No. Paging on 1032 means waking an engineer every time a customer changes their mind about a payment. Log it at info level, show the customer a 'Payment cancelled' message with a retry option, and track it separately in analytics as a cancellation. Reserve alerts for codes that indicate the platform or your integration actually malfunctioned.
- What is the difference between M-Pesa 1032 and 1037?
- 1032 means the user actively cancelled the prompt. 1037 means the prompt never got a decision at all, usually a timeout or an unreachable phone. Both end without payment, but they mean different things about the customer's intent, so keep them in separate analytics buckets. Neither one should trigger an alert.
- How should I let a customer retry after a 1032 cancellation?
- Start a completely fresh STK push with a new CheckoutRequestID when they tap retry. Do not try to reuse or resume the cancelled request, since it is finished. If the customer retries too quickly you may hit error 1001 'transaction in progress', so allow a short delay before firing the new push.