Dev.to WebDev πŸ›  Dev πŸ‘ 0 πŸ“– 3 min read

M-Pesa ResultCode 1037 doesn't mean the payment failed

If you've integrated M-Pesa STK push (Daraja's "Lipa na M-Pesa Online"), you've probably written something like this: if (callback.ResultCode === 0) markPaid(order); else markFailed(order); It works in the sandb

If you've integrated M-Pesa STK push (Daraja's "Lipa na M-Pesa Online"), you've probably written something like this:

if (callback.ResultCode === 0) markPaid(order);
else markFailed(order);

It works in the sandbox. In production it quietly causes three problems:
Callbacks go missing, so paid orders sit at "pending" forever.
Callbacks can be faked, because Daraja doesn't sign them.
ResultCode 1037 gets treated as a failure when it often isn't, and customers who already paid are told to pay again.
What 1037 actually means
1037 is "DS timeout: user cannot be reached". Safaricom sent the PIN prompt, but didn't hear back from the phone in time: it was off, had no signal, or the customer was slow.
That is not the same as "the customer didn't pay". Developers report payments that completed minutes after a 1037 callback. If your code marks the order failed and shows a "Try again" button, a customer who did pay can end up paying twice.
Sort ResultCodes into three groups, not two
Group ResultCodes What to do
Paid 0 Confirm with STK Push Query, check the amount, record the receipt, fulfil once
Failed, final 1 (low balance), 1032 (cancelled), 2001 (wrong PIN), 1019 (expired) Mark failed and offer "Try again"
Not known yet 1037, 1001 (another transaction in progress), 1025, 17, 26, and anything you don't recognise Keep pending, query again later, and don't send a second push yet
The third group is the whole point. Waiting a few minutes costs you nothing. Wrongly failing a paid order costs you a customer.
In code, that's one small function:

const FINAL_FAILED = new Set(['1', '1032', '2001', '1019']);

/** paid | failed | unknown. Anything not clearly final is "unknown": ask again. */
export function classify(resultCode) {
  const code = resultCode == null ? '' : String(resultCode);
  if (code === '0') return 'paid';
  if (FINAL_FAILED.has(code)) return 'failed';
  return 'unknown'; // 1037, 1001, 1025, 17, 26, no answer yet, and anything new
}

For a 1037, keep the payment pending, tell the customer you're checking, and query it with STK Push Query after about 30 seconds, then with longer gaps (60 s, 2 min, 5 min, 10 min). If it's still unknown after that, flag it for a person to check against your M-Pesa statement. And don't allow a second push for the same order until the first one has a final answer.
Why callbacks go missing
When the callback never arrives, it's almost always one of these:
The URL isn't public HTTPS (Safaricom can't reach localhost).
The URL contains "mpesa" or "safaricom". Developers report these are silently dropped; use a neutral path like /webhooks/stk.
A redirect (http β†’ https, or a trailing slash). Safaricom doesn't follow them.
Your server is slow or returns an error. Acknowledge immediately, do the work afterwards.
The fix is the same in every case: don't rely on the callback alone. Query every push that is still pending after a minute or two.
Callbacks can be faked
Daraja callbacks aren't signed. Anyone who finds your callback URL can post "ResultCode": 0. So treat the callback as a hint: find the payment, then confirm with STK Push Query before marking anything paid.
One catch: the query response doesn't include the receipt number. Take MpesaReceiptNumber from the callback, but only after the query confirms the payment. A forged callback then can't mark an order paid.
Settle each payment exactly once
Callbacks arrive twice, and your retry job can run at the same moment as a late callback. Let the database decide who wins:

UPDATE mpesa_payments
SET status = 'paid', receipt = $2, updated_at = now()
WHERE checkout_request_id = $1 AND status = 'pending';
-- 1 row: you settled it, fulfil the order. 0 rows: someone else did. Do nothing.

The full version
I wrote this up in full, with a tested Node.js module (classify, safe callback parsing, a query schedule, and a test suite), on PayDev Africa: M-Pesa STK Push Callbacks and ResultCode 1037. Every ResultCode is explained in the M-Pesa error code reference, and there are versions of the STK push integration for Node.js, Laravel and Django.
How do you handle 1037 in your integration? I'd like to hear what's worked for you.

πŸ“° Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.