Complete M-Pesa Integration Guide for African Developers
M-Pesa's Daraja API powers mobile payments across Kenya, Tanzania, and several other East African markets. Getting the integration right the first time saves weeks of debugging and prevents the kind of payment failures that destroy user trust. This guide walks through the full implementation — from getting your credentials to handling edge cases in production.
Understanding the Daraja API: The Three Payment Flows
Before writing any code, understand which payment flow your application needs. Daraja offers three primary patterns:
- STK Push (Lipa na M-Pesa Online / M-Pesa Express): Your server triggers a payment prompt directly on the customer's phone. The customer sees a PIN dialog and authorizes the payment without leaving your app. This is the most common integration for e-commerce and service apps.
- C2B (Customer to Business): The customer initiates the payment themselves by sending to your Paybill or Buy Goods shortcode. Your server receives a real-time notification when payment arrives. Used for bill payments, school fees, and scenarios where customers pay on their own timeline.
- B2C (Business to Customer): Your system sends money to a customer's M-Pesa number. Used for salary disbursements, refunds, and marketplace seller payouts. Requires explicit Safaricom approval and higher compliance requirements.
Step 1: Getting Your Credentials
Register at developer.safaricom.co.ke and create an app. You will receive:
- Consumer Key — your app's public identifier
- Consumer Secret — your app's private credential (never expose this to a browser or mobile client)
- Business Shortcode — your Paybill or Till number (use
174379for sandbox testing) - Passkey — a long string provided by Safaricom, used to generate the STK Push password (use the sandbox passkey for testing: it is displayed in the Daraja developer portal)
Store all credentials as environment variables. Never commit them to source control.
Step 2: Getting an Access Token
All Daraja API calls require a Bearer token obtained via OAuth. The token endpoint is https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials (replace sandbox with api for production).
Make a GET request with a Basic Auth header where the credentials are Base64(ConsumerKey:ConsumerSecret). The response gives you an access_token valid for 3600 seconds. Cache this token and refresh it before it expires — generating a new token on every API call is wasteful and can trigger rate limits.
GET /oauth/v1/generate?grant_type=client_credentials
Authorization: Basic Base64(consumerKey:consumerSecret)
Response:
{
"access_token": "SGWcJPtNtYNPGm77...",
"expires_in": "3599"
}
Step 3: STK Push — The Full Implementation
The STK Push endpoint is POST /mpesa/stkpush/v1/processrequest. The trickiest part is generating the Password field correctly.
The password is Base64(Shortcode + Passkey + Timestamp) where Timestamp is in the format YYYYMMDDHHmmss in East Africa Time (UTC+3). This is the most common source of errors for developers new to Daraja.
const timestamp = new Date()
.toISOString()
.replace(/[-:T.Z]/g, "")
.slice(0, 14);
// Result: "20250115143022" (EAT time, not UTC!)
const password = Buffer.from(
shortcode + passkey + timestamp
).toString("base64");
The full request body:
{
"BusinessShortCode": "174379",
"Password": "Base64(shortcode+passkey+timestamp)",
"Timestamp": "20250115143022",
"TransactionType": "CustomerPayBillOnline",
"Amount": 1,
"PartyA": "254712345678",
"PartyB": "174379",
"PhoneNumber": "254712345678",
"CallBackURL": "https://yourdomain.com/api/mpesa/callback",
"AccountReference": "Order-1234",
"TransactionDesc": "Payment for Order 1234"
}
A successful initiation response gives you a CheckoutRequestID. Store this immediately in your database alongside the order. This ID is how you match the callback to the transaction.
Step 4: Handling the Callback
After the customer enters their PIN (or cancels), Safaricom sends a POST request to your CallBackURL. Your callback endpoint must:
- Be accessible over HTTPS (self-signed certificates will not work in production)
- Respond with HTTP 200 within 5 seconds — if you take longer, Safaricom may retry
- Be idempotent — handle duplicate callbacks gracefully without double-crediting an account
A successful payment callback looks like this:
{
"Body": {
"stkCallback": {
"MerchantRequestID": "29115-34620561-1",
"CheckoutRequestID": "ws_CO_191220191020363925",
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
{ "Name": "Amount", "Value": 1.00 },
{ "Name": "MpesaReceiptNumber", "Value": "NLJ7RT61SV" },
{ "Name": "TransactionDate", "Value": 20191219102115 },
{ "Name": "PhoneNumber", "Value": 254708374149 }
]
}
}
}
}
A cancelled or failed payment returns ResultCode: 1032 (cancelled by user) or other non-zero codes. Always check ResultCode === 0 before crediting the account.
The idempotency pattern: before processing any callback, check if the MpesaReceiptNumber already exists in your transactions table. If it does, respond 200 but take no action. This prevents double-crediting when Safaricom retries a callback.
Step 5: What to Do When the Callback Never Arrives
Callbacks can fail for many reasons — the customer's phone is off, your server had a brief outage, or there was a network hiccup between Safaricom and your endpoint. Never leave a transaction in "pending" state indefinitely.
The solution is the STK Push Query endpoint: POST /mpesa/stkpushquery/v1/query. Send the CheckoutRequestID and Daraja returns the current transaction status. Build a background job that queries any transaction still in "pending" state after 2 minutes.
Common Errors and How to Fix Them
Error 400.002.02 — Invalid Access Token
Your token has expired (they last 3599 seconds) or you are using a sandbox token against the production endpoint. Implement token caching with expiry-aware refresh.
Error 400.002.05 — Invalid Timestamp Format
Your timestamp is either in the wrong format or in the wrong timezone. The format must be exactly YYYYMMDDHHmmss and must reflect East Africa Time (UTC+3), not your server's local timezone or UTC.
Error 400.002.06 — Invalid Password
The Base64 encoding of your password is wrong. The most common cause is using the wrong passkey (e.g., production passkey in sandbox or vice versa), or including extra whitespace characters in the string before encoding.
Callback URL Not Reachable
Safaricom validates your callback URL during the STK Push call. If your server is not publicly accessible (e.g., running on localhost), the initiation will fail. Use a tunneling tool like ngrok during development to expose your local server. In production, ensure your server responds to HTTPS and your firewall allows inbound requests from Safaricom's IP ranges.
Result Code 1 — Insufficient Funds
The customer's M-Pesa balance is too low. Present a clear error message and offer the customer the option to try a different amount or top up. Do not retry automatically.
C2B Integration: Registering Validation and Confirmation URLs
For C2B flows (customer pays to your Paybill/Till), you must register two URLs with Safaricom using the /mpesa/c2b/v1/registerurl endpoint:
- ValidationURL: Called before payment is processed. Return
{"ResultCode": "0"}to accept or{"ResultCode": "C2B00011"}to reject. - ConfirmationURL: Called after payment is confirmed. Must respond 200 within 5 seconds.
URL registration only needs to happen once per shortcode — not on every deployment. Store whether URLs are registered in your database.
Going Live: The Production Checklist
- Switch all API base URLs from
sandbox.safaricom.co.ketoapi.safaricom.co.ke - Replace sandbox credentials with production Consumer Key, Consumer Secret, Shortcode, and Passkey
- Ensure callback URLs use production HTTPS domains with valid SSL certificates
- Test a live transaction of KSh 1 end-to-end before going live
- Implement transaction logging — store every request and callback in your database with timestamps
- Set up alerting for high failure rates or pending transactions older than 10 minutes
- Configure retry logic for callback processing failures (your server might process the callback but fail to update the database)
- Document your reconciliation process — how will you match M-Pesa receipts to orders at end of day?
Production Tips from Real Deployments
After handling M-Pesa integrations across dozens of applications, these patterns consistently improve reliability:
- Use a dedicated webhook processing queue (Redis, a database queue table, or a managed queue service) so callback processing does not block your web server.
- Log the full raw callback JSON before doing any processing — you want the original data if something goes wrong downstream.
- Build a reconciliation report that compares your order database against M-Pesa's transaction list (downloadable from the business portal) weekly. Discrepancies happen.
- Never show the user "payment successful" until your server has confirmed the callback was processed and the database was updated. Show "processing" until the confirmation.
Need help implementing M-Pesa into your application? HazinaPay specialises in payment integrations for East African applications. Get a free quote and we will scope your integration properly from the start.
Ready to build your software?
Get an instant, detailed quote in 60 seconds — no calls, no commitment.
Get a Free Quote →