Skip to content

STK Push

Most Pesapal integrations send the customer to a hosted payment page and wait for them to come back. An STK push skips it: Pesapal sends a PIN prompt straight to the customer’s phone, and they approve the payment without ever leaving your app.

The name comes from M-Pesa, but the same endpoint pushes prompts to Airtel Money in Kenya and Uganda.

Use it when a redirect would break the experience, which is most of the time in a mobile app, and when you already know the customer’s phone number.

Hosted page STK push
Customer leaves your app Yes No
Payment methods Cards, M-Pesa, Airtel Money M-Pesa and Airtel Money only
You need their phone number first No Yes
Response gives you A redirect_url A prompt already sent
Terminal window
curl --request POST \
--url https://cybqa.pesapal.com/pesapalv3/api/transactions/stk \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
--header 'Content-Type: application/json' \
--data '{
"id": "ORDER-1234",
"currency": "KES",
"amount": 100,
"description": "Test payment",
"callback_url": "https://example.com/payment-complete",
"notification_id": "YOUR_IPN_ID",
"billing_address": {
"email_address": "customer@example.com"
},
"msisdn": "0712345678",
"payment_method": "MpesaKE"
}'

Full schema: directMobileMoneyStk.

The body is an ordinary order plus two fields:

Field Required What it does
msisdn Yes The phone number to prompt.
payment_method Yes MpesaKE, AirtelKE or AirtelUG.

A successful push returns:

{
"business_number": "220222",
"payment_message": "Request processed successfully",
"account_number": "92266923",
"order_tracking_id": "68691e1c-e014-44ad-a9ee-d9eefc38f1b3",
"merchant_reference": "2962192609085431",
"redirect_url": null,
"error": null,
"status": "200"
}

There is no redirect_url, because there is nowhere to send anyone. What you get instead is business_number and account_number: the paybill and account reference the customer can use to pay manually.

Show both on screen. Prompts get missed, dismissed by accident, or arrive while the phone is off. Displaying the paybill details is the difference between a customer completing the payment another way and abandoning it.

The prompt arrives on the handset within a second or two. It names the amount and the payee and asks for the M-Pesa PIN, and that is all. Your description, your id and the account reference do not appear on it.

That has a practical consequence: the prompt gives the customer almost nothing to recognise the payment by. Tell them what to expect before you send it, and show the amount on your own screen so the two match.

They can approve it, decline it, or ignore it until it times out.

Confirm exactly as you would for a hosted payment, using the order_tracking_id and GetTransactionStatus.

{
"payment_method": "MpesaKE",
"amount": 100,
"confirmation_code": "24152319",
"payment_status_description": "Failed",
"description": null,
"payment_account": "2547xxx02697",
"status_code": 2,
"merchant_reference": "ORDER-1234",
"payment_status_code": "request_terminated_by_user",
"currency": "KES"
}

That is a payment the customer declined. A completed one differs only in the fields that describe the outcome:

{
"confirmation_code": "UI76G5D93D",
"payment_status_description": "Completed",
"status_code": 1,
"payment_status_code": ""
}

Three fields are worth knowing here.

status_code is the outcome: 1 completed, 2 failed. merchant_reference is your id, correctly, unlike in the push response. And payment_status_code sometimes names the reason for a failure, which is the only way to tell a customer who refused from a prompt that never arrived:

Situation payment_status_code
Completed ""
Customer declined the prompt request_terminated_by_user
Number unreachable ""

Note it is empty in two of those three, so treat a value as useful detail rather than something to depend on.