M-Pesa Daraja API Integration: The Complete Walkthrough for Developers
Daraja is Safaricom's REST API that lets your application trigger M-Pesa payments, query transaction status, and receive payment confirmations via callbacks. The core flow is: get an OAuth token, send an STK Push request with the customer's phone number and amount, then listen on a callback URL for the payment result. Everything below uses real endpoints, and the code runs as-is against the sandbox.
What is the Daraja API?
Daraja (Swahili for "bridge") is Safaricom's gateway for developers to integrate M-Pesa into applications. It replaced the older G2 API and is the only officially supported way to process M-Pesa payments programmatically.
Daraja exposes several products:
- Lipa na M-Pesa Online (STK Push): triggers the payment prompt on the customer's phone
- C2B: registers URLs to receive payment notifications when a customer pays a paybill or till
- B2C: sends money from your business to a customer's M-Pesa
- B2B: transfers between business accounts
- Transaction Status: queries whether a payment went through
- Account Balance: checks the balance on your M-Pesa shortcode
For most Kenyan startups, the journey begins with STK Push. That is what we build in this guide.
Prerequisites and sandbox setup
Before writing any code, you need credentials from the Daraja portal.
- Go to
developer.safaricom.co.keand create an account. - Create a new app. Select both Lipa Na M-Pesa Sandbox and APIs when prompted.
- Copy your Consumer Key and Consumer Secret from the app dashboard.
- Note the sandbox test credentials: shortcode, passkey, and test phone number (usually 254708374149).
Store these in environment variables. Never hardcode credentials in source files.
# .env
DARAJA_CONSUMER_KEY=your_consumer_key_here
DARAJA_CONSUMER_SECRET=your_consumer_secret_here
DARAJA_SHORTCODE=174379
DARAJA_PASSKEY=bfb279f9aa9bdbcf158e97dd71a467cd2e0c893059b10f78e6b72ada1ed2c919
DARAJA_CALLBACK_URL=https://your-tunnel.ngrok-free.app/api/mpesa/callbackYou will also need a tunnel tool like ngrok to expose your local server so Safaricom can reach your callback URL during development. Install it with npm install -g ngrok or download from the ngrok website.
Step 1: Get an OAuth access token
Every Daraja request needs a Bearer token. The token is valid for 3600 seconds (one hour), so you should cache it and refresh before expiry.
Node.js (Express + axios):
import axios from 'axios';
const DARAJA_AUTH_URL = 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials';
async function getAccessToken(): Promise<string> {
const auth = Buffer.from(
`${process.env.DARAJA_CONSUMER_KEY}:${process.env.DARAJA_CONSUMER_SECRET}`
).toString('base64');
const response = await axios.get(DARAJA_AUTH_URL, {
headers: { Authorization: `Basic ${auth}` },
});
return response.data.access_token;
}Python (Flask + requests):
import requests
import os
from base64 import b64encode
DARAJA_AUTH_URL = "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials"
def get_access_token() -> str:
key = os.environ["DARAJA_CONSUMER_KEY"]
secret = os.environ["DARAJA_CONSUMER_SECRET"]
credentials = b64encode(f"{key}:{secret}".encode()).decode()
response = requests.get(
DARAJA_AUTH_URL,
headers={"Authorization": f"Basic {credentials}"},
)
response.raise_for_status()
return response.json()["access_token"]The sandbox URL starts with sandbox.safaricom.co.ke. When you go live, swap it for api.safaricom.co.ke. Keep the base URL in an environment variable so you can switch without code changes.
Step 2: Trigger an STK Push
The STK Push (Lipa na M-Pesa Online) sends a payment prompt directly to the customer's phone. They see a dialog asking them to enter their M-Pesa PIN to authorize the payment.
You need to generate a password by Base64-encoding the shortcode, passkey, and a timestamp together.
Node.js:
import axios from 'axios';
const STK_PUSH_URL = 'https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest';
async function initiateSTKPush(phone: string, amount: number) {
const token = await getAccessToken();
const timestamp = new Date()
.toISOString()
.replace(/[-T:.Z]/g, '')
.slice(0, 14);
const shortcode = process.env.DARAJA_SHORTCODE!;
const passkey = process.env.DARAJA_PASSKEY!;
const password = Buffer.from(
`${shortcode}${passkey}${timestamp}`
).toString('base64');
const payload = {
BusinessShortCode: shortcode,
Password: password,
Timestamp: timestamp,
TransactionType: 'CustomerPayBillOnline',
Amount: Math.round(amount), // always whole KES
PartyA: phone,
PartyB: shortcode,
PhoneNumber: phone,
CallBackURL: process.env.DARAJA_CALLBACK_URL,
AccountReference: 'McTabaLabs',
TransactionDesc: 'Course payment',
};
const response = await axios.post(STK_PUSH_URL, payload, {
headers: { Authorization: `Bearer ${token}` },
});
return response.data;
}Python:
import requests
import os
from base64 import b64encode
from datetime import datetime
STK_PUSH_URL = "https://sandbox.safaricom.co.ke/mpesa/stkpush/v1/processrequest"
def initiate_stk_push(phone: str, amount: int) -> dict:
token = get_access_token()
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
shortcode = os.environ["DARAJA_SHORTCODE"]
passkey = os.environ["DARAJA_PASSKEY"]
password = b64encode(
f"{shortcode}{passkey}{timestamp}".encode()
).decode()
payload = {
"BusinessShortCode": shortcode,
"Password": password,
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": amount,
"PartyA": phone,
"PartyB": shortcode,
"PhoneNumber": phone,
"CallBackURL": os.environ["DARAJA_CALLBACK_URL"],
"AccountReference": "McTabaLabs",
"TransactionDesc": "Course payment",
}
response = requests.post(
STK_PUSH_URL,
json=payload,
headers={"Authorization": f"Bearer {token}"},
)
response.raise_for_status()
return response.json()A successful response returns a CheckoutRequestID which you should store in your database. This is the reference you use to match the callback to the original request.
Important: the Amount field must be a whole number. M-Pesa does not process fractional KES. If your internal system stores cents, convert to whole shillings before calling this endpoint.
Step 3: Handle the callback
After the customer enters their PIN (or the request times out), Safaricom sends a POST request to your CallBackURL. This is how you know whether the payment succeeded or failed.
Node.js (Express):
import express from 'express';
const app = express();
app.use(express.json());
app.post('/api/mpesa/callback', (req, res) => {
const { Body } = req.body;
const resultCode = Body.stkCallback.ResultCode;
const checkoutRequestId = Body.stkCallback.CheckoutRequestID;
if (resultCode === 0) {
// Payment successful
const items = Body.stkCallback.CallbackMetadata.Item;
const amount = items.find((i: any) => i.Name === 'Amount')?.Value;
const receipt = items.find(
(i: any) => i.Name === 'MpesaReceiptNumber'
)?.Value;
const phone = items.find(
(i: any) => i.Name === 'PhoneNumber'
)?.Value;
console.log(`Payment of KES ${amount} received.`);
console.log(`Receipt: ${receipt}, Phone: ${phone}`);
// TODO: update your database order status
} else {
// Payment failed or was cancelled
console.log(`Payment failed. Code: ${resultCode}`);
}
// Always respond with 200 to acknowledge receipt
res.status(200).json({ ResultCode: 0, ResultDesc: 'Accepted' });
});
app.listen(3000, () => console.log('Server running on port 3000'));Python (Flask):
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/mpesa/callback", methods=["POST"])
def mpesa_callback():
data = request.get_json()
result_code = data["Body"]["stkCallback"]["ResultCode"]
checkout_id = data["Body"]["stkCallback"]["CheckoutRequestID"]
if result_code == 0:
items = data["Body"]["stkCallback"]["CallbackMetadata"]["Item"]
amount = next(i["Value"] for i in items if i["Name"] == "Amount")
receipt = next(
i["Value"] for i in items if i["Name"] == "MpesaReceiptNumber"
)
phone = next(
i["Value"] for i in items if i["Name"] == "PhoneNumber"
)
print(f"Payment of KES {amount} received. Receipt: {receipt}")
# TODO: update your database order status
else:
print(f"Payment failed. Code: {result_code}")
return jsonify({"ResultCode": 0, "ResultDesc": "Accepted"}), 200
if __name__ == "__main__":
app.run(port=3000)Critical points about callbacks:
- Always respond with HTTP 200 immediately. If you return an error or take too long (more than 5 seconds), Safaricom may retry or mark the callback as failed.
- Do heavy processing (database writes, email notifications) asynchronously. Acknowledge first, process second.
- The callback may arrive multiple times. Use the
CheckoutRequestIDas an idempotency key to avoid double-processing. - During development, use ngrok to expose your local server:
ngrok http 3000. Copy the HTTPS URL into your.envfile.
Testing and next steps
With the sandbox credentials, you can trigger STK Push requests and receive callbacks without real money changing hands. The sandbox test phone number will automatically "approve" the payment.
A basic test flow:
- Start your server:
npx ts-node server.ts(Node.js) orpython app.py(Python) - Start ngrok:
ngrok http 3000 - Update your
.envwith the ngrok HTTPS URL - Call your STK Push endpoint (use curl, Postman, or a simple frontend form)
- Watch the callback arrive in your server logs
Once this flow works end to end, you are ready to move to production. The go-live process involves applying for shortcode approval, swapping sandbox URLs for production URLs, and passing Safaricom's compliance review. See our Daraja Sandbox to Production checklist for the full process.
Other Daraja products you should explore next:
- Transaction Status Query: poll for payment status when callbacks fail to arrive (network issues happen)
- C2B registration: get notified when customers pay your paybill or till directly from the M-Pesa app
- B2C disbursement: send refunds or payouts to customers
Frequently Asked Questions
- Is the Daraja API free to use?
- The sandbox is free. In production, Safaricom charges standard M-Pesa transaction fees on each payment. There is no separate API subscription fee. The transaction fees depend on your shortcode type and the amount being transacted. [TODO: verify on provider website] for current fee schedules.
- Can I use Daraja from outside Kenya?
- Yes, the API is accessible from anywhere. You can host your server in any country. However, the customer making the payment must have a Safaricom M-Pesa account (Kenyan phone number starting with 254). For other countries, look at the respective M-Pesa APIs from Vodacom (Tanzania) or Airtel Money.
- How long does the STK Push dialog stay on the customer phone?
- The customer has approximately 60 seconds to enter their PIN. If they do not respond, the request times out and the callback returns a result code indicating cancellation or timeout. You should show your user a "waiting for payment" state and handle the timeout gracefully.
- What phone number format does Daraja expect?
- Use the international format without the plus sign: 254XXXXXXXXX. For example, a Kenyan number 0712345678 becomes 254712345678. Do not include the leading zero or any spaces.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon. Ship 8 production apps with M-Pesa, USSD, and WhatsApp integrations, and get career support until placement.
See Programs