Integrate with Safaricom's M-Pesa Daraja API for Kenyan mobile money payments. Use this skill whenever the user wants to accept M-Pesa payments, trigger STK push prompts, handle C2B or B2C transactions, send money to M-Pesa users, check account balances, or work with M-Pesa/Daraja in any way. Also trigger for 'M-Pesa', 'Daraja', 'Safaricom API', 'STK push', 'Lipa Na M-Pesa', 'KES mobile money', 'send money to M-Pesa', 'Kenyan mobile payments', or any mention of collecting or disbursing payments through M-Pesa in Kenya.
M-Pesa is East Africa's dominant mobile money platform with 50M+ active users in Kenya alone. The Daraja API is Safaricom's official gateway to M-Pesa — it lets you collect payments (STK Push, C2B), send money (B2C), and query transactions programmatically.
You're building something that needs to move money in Kenya via M-Pesa — collecting payments for goods/services, disbursing funds to users, building a marketplace with M-Pesa checkout, or integrating Lipa Na M-Pesa into an app. M-Pesa is how most Kenyans transact, so if your user base is in Kenya, you probably need this.
Daraja uses OAuth 2.0. You get an access token from a consumer key + consumer secret pair, then use the token for all subsequent requests.
GET /oauth/v1/generate?grant_type=client_credentials
Authorization: Basic {base64(consumer_key:consumer_secret)}
Response:
{
"access_token": "xxxxx",
"expires_in": "3599"
}
Token expires in 1 hour. Cache it and refresh before expiry.
Environment URLs:
https://sandbox.safaricom.co.kehttps://api.safaricom.co.keStore credentials in env vars: MPESA_CONSUMER_KEY, MPESA_CONSUMER_SECRET, MPESA_PASSKEY, MPESA_SHORTCODE.
The most common integration — triggers a payment prompt on the customer's phone. They enter their M-Pesa PIN to complete the payment.
POST /mpesa/stkpush/v1/processrequest
Authorization: Bearer {access_token}
{
"BusinessShortCode": "174379",
"Password": "{base64(shortcode + passkey + timestamp)}",
"Timestamp": "20250115143000",
"TransactionType": "CustomerPayBillOnline",
"Amount": 1000,
"PartyA": "254712345678",
"PartyB": "174379",
"PhoneNumber": "254712345678",
"CallBackURL": "https://yoursite.com/api/mpesa/callback",
"AccountReference": "OrderRef123",
"TransactionDesc": "Payment for Order #123"
}
Password generation:
const timestamp = new Date().toISOString().replace(/[-T:.Z]/g, '').slice(0, 14);
const password = Buffer.from(`${shortcode}${passkey}${timestamp}`).toString('base64');
Important:
Amount is in whole KES (not cents). KES 1000 = 1000.254XXXXXXXXX (no + prefix, no leading 0).TransactionType: Use CustomerPayBillOnline for paybill, CustomerBuyGoodsOnline for till numbers.Response:
{
"MerchantRequestID": "xxxxx",
"CheckoutRequestID": "ws_CO_xxxxx",
"ResponseCode": "0",
"ResponseDescription": "Success. Request accepted for processing",
"CustomerMessage": "Success. Request accepted for processing"
}
ResponseCode: "0" means the request was accepted — not that payment succeeded. You need to wait for the callback or query the status.
POST /mpesa/stkpushquery/v1/query
{
"BusinessShortCode": "174379",
"Password": "{base64(shortcode + passkey + timestamp)}",
"Timestamp": "20250115143000",
"CheckoutRequestID": "ws_CO_xxxxx"
}
Response (success):
{
"ResponseCode": "0",
"ResultCode": "0",
"ResultDesc": "The service request is processed successfully.",
"MerchantRequestID": "xxxxx",
"CheckoutRequestID": "ws_CO_xxxxx"
}
ResultCode: "0" = payment successful. ResultCode: "1032" = user cancelled. ResultCode: "1037" = timeout (user didn't respond).
Register URLs to receive payment notifications when customers pay your paybill/till. This is critical for receiving callbacks when customers send money to your business account.
POST /mpesa/c2b/v1/registerurl
{
"ShortCode": "600000",
"ResponseType": "Completed",
"ConfirmationURL": "https://yoursite.com/api/mpesa/confirm",
"ValidationURL": "https://yoursite.com/api/mpesa/validate"
}
Important:
ValidationURL is called before the transaction completes — return {"ResultCode": 0} to accept or {"ResultCode": 1} to reject.ConfirmationURL receives the final payment confirmation after the transaction is complete.M-Pesa POSTs to your validation URL before completing the transaction:
{
"TransactionType": "Pay Bill Online",
"TransID": "LIB221011CD6D",
"TransTime": "20221011095810",
"TransAmount": 1000,
"BusinessShortCode": "600000",
"BillRefNumber": "OrderRef123",
"InvoiceNumber": "",
"OrgAccountBalance": "100000.00",
"ThirdPartyTransID": "",
"MSISDN": "254712345678",
"FirstName": "John",
"MiddleName": "",
"LastName": "Doe"
}
Your response:
{
"ResultCode": 0,
"ResultDesc": "Accepted"
}
Return ResultCode: 0 to accept, ResultCode: 1 to reject the payment.
M-Pesa POSTs to your confirmation URL after the transaction is complete:
{
"TransactionType": "Pay Bill Online",
"TransID": "LIB221011CD6D",
"TransTime": "20221011095810",
"TransAmount": 1000,
"BusinessShortCode": "600000",
"BillRefNumber": "OrderRef123",
"InvoiceNumber": "",
"OrgAccountBalance": "100000.00",
"ThirdPartyTransID": "",
"MSISDN": "254712345678",
"FirstName": "John",
"MiddleName": "",
"LastName": "Doe"
}
Always respond with HTTP 200 to M-Pesa callbacks, even if you encounter processing errors — otherwise M-Pesa will keep retrying.
Send money from your M-Pesa business account to a customer's phone:
POST /mpesa/b2c/v1/paymentrequest
{
"InitiatorName": "testapi",
"SecurityCredential": "{encrypted_password}",
"CommandID": "BusinessPayment",
"Amount": 500,
"PartyA": "600000",
"PartyB": "254712345678",
"Remarks": "Salary payment",
"QueueTimeOutURL": "https://yoursite.com/api/mpesa/timeout",
"ResultURL": "https://yoursite.com/api/mpesa/b2c/result",
"Occasion": "January salary"
}
CommandID options:
BusinessPayment — normal B2C paymentSalaryPayment — salary disbursementPromotionPayment — promotional paymentSecurityCredential: Your initiator password encrypted with Safaricom's RSA public key certificate. Download the cert from the Daraja portal and encrypt:
const fs = require('fs');
const crypto = require('crypto');
const cert = fs.readFileSync('ProductionCertificate.cer');
const encrypted = crypto.publicEncrypt(
{ key: cert, padding: crypto.constants.RSA_PKCS1_PADDING },
Buffer.from(password)
);
const securityCredential = encrypted.toString('base64');
Response:
{
"ConversationID": "AG_20250115_XXXXX",
"OriginatorConversationID": "XXXXX",
"ResponseCode": "0",
"ResponseDescription": "Accept the service request successfully."
}
M-Pesa POSTs to your ResultURL after processing the B2C payment:
{
"Result": {
"ResultType": 0,
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"OriginatorConversationID": "XXXXX",
"ConversationID": "AG_20250115_XXXXX",
"TransactionID": "OEI2AK4Q16",
"ReferenceData": {
"ReferenceItem": {
"Key": "QueueTimeoutURL",
"Value": "https://yoursite.com/api/mpesa/timeout"
}
},
"ResultParameters": {
"ResultParameter": [
{
"Key": "TransactionAmount",
"Value": 500
},
{
"Key": "TransactionReceipt",
"Value": "OEI2AK4Q16"
},
{
"Key": "ReceiverPartyPublicName",
"Value": "254712345678"
},
{
"Key": "TransactionCompletedDateTime",
"Value": "20250115143025"
},
{
"Key": "OriginatorAccountBalance",
"Value": "100000.00"
}
]
}
}
}
Check Result.ResultCode: 0 for success.
POST /mpesa/transactionstatus/v1/query
{
"Initiator": "testapi",
"SecurityCredential": "{encrypted}",
"CommandID": "TransactionStatusQuery",
"TransactionID": "OEI2AK4Q16",
"PartyA": "600000",
"IdentifierType": "4",
"ResultURL": "https://yoursite.com/api/mpesa/status/result",
"QueueTimeOutURL": "https://yoursite.com/api/mpesa/timeout",
"Remarks": "Check status",
"Occasion": "Status check"
}
IdentifierType:
1 = MSISDN (phone number)2 = Organization short code4 = Organization short code (for business accounts)POST /mpesa/accountbalance/v1/query
{
"Initiator": "testapi",
"SecurityCredential": "{encrypted}",
"CommandID": "AccountBalance",
"PartyA": "600000",
"IdentifierType": "4",
"Remarks": "Balance check",
"QueueTimeOutURL": "https://yoursite.com/api/mpesa/timeout",
"ResultURL": "https://yoursite.com/api/mpesa/balance/result"
}
Reverse a completed M-Pesa transaction:
POST /mpesa/reversal/v1/request
{
"Initiator": "testapi",
"SecurityCredential": "{encrypted}",
"CommandID": "TransactionReversal",
"TransactionID": "OEI2AK4Q16",
"Amount": 500,
"ReceiverParty": "600000",
"ReceiverIdentifierType": "11",
"ResultURL": "https://yoursite.com/api/mpesa/reversal/result",
"QueueTimeOutURL": "https://yoursite.com/api/mpesa/timeout",
"Remarks": "Wrong amount",
"Occasion": "Reversal"
}
When an STK Push transaction completes, M-Pesa POSTs to your CallbackURL:
{
"Body": {
"stkCallback": {
"MerchantRequestID": "xxxxx",
"CheckoutRequestID": "ws_CO_xxxxx",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 1000 },
{ "Name": "MpesaReceiptNumber", "Value": "OEI2AK4Q16" },
{ "Name": "TransactionDate", "Value": 20250115143025 },
{ "Name": "PhoneNumber", "Value": 254712345678 }
]
}
}
}
}
Key ResultCode values:
0 — Success, money received1 — Not enough balance on user's M-Pesa account1032 — User cancelled the transaction1037 — Transaction timeout (user didn't respond within ~60 seconds)2001 — Wrong PIN entered by userAlways respond with HTTP 200 to callbacks, even if you encounter errors — otherwise M-Pesa will keep retrying (up to ~3 times over 30 minutes).
CustomerPayBillOnline for STK Push.CustomerBuyGoodsOnline for STK Push.https://sandbox.safaricom.co.ke (uses test credentials)https://api.safaricom.co.ke (uses live credentials)254XXXXXXXXX (no +, no leading 0).254 is Kenya's country code. All numbers must start with it."0" = accepted for processing.0 = success.POST /mpesa/stkpush/v1/processrequest with their phone numberResultCode === 0 → fulfill orderPOST /mpesa/b2c/v1/paymentrequest to pay each sellerResultCode: 500.001.1001 (insufficient funds) gracefullyResultCode: 1032 (cancel) → update subscription statusResultCode: 1037 (timeout) → retry or notify userPOST /mpesa/c2b/v1/registerurlM-Pesa error responses vary by endpoint but follow this pattern:
{
"requestId": "xxxxx",
"errorCode": "400.002.02",
"errorMessage": "Bad Request - Invalid Amount"
}
Common ResultCode error values in callbacks:
Sync response error codes:
When you receive an error:
254... required)Daraja sandbox credentials are available at https://developer.safaricom.co.ke/test_credentials
Test phone numbers: Use any 254XXXXXXXXX number in sandbox. The sandbox simulates successful payments by default for testing.
Sandbox limitations: