Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin•11/5/2026•15 min read

M-Pesa Daraja API Integration: Complete Developer Guide

M-Pesa Daraja API Integration: Complete Developer Guide

# M-Pesa API Integration: Complete Developer Guide

Summary: This comprehensive guide details how software engineers and product teams execute direct M-Pesa API integration using Safaricom's Daraja platform in Kenya. It covers sandbox setup, OAuth 2.0 token caching, complete Node.js implementations for Lipa Na M-Pesa Online (STK push), robust asynchronous webhook callback handling, Central Bank of Kenya (CBK) compliance limits, and production go-live verification.

---

The Strategic Context: Why Direct Daraja Integration Matters in Kenya

Kenya is globally recognized as the pioneer of mobile money. According to the Central Bank of Kenya (CBK) National Payments System statistical releases, annual national mobile money transactions across licensed payment service providers total between KES 7.9 trillion and KES 8.7 trillion. Meanwhile, Safaricom's published Annual Reports indicate that the broader M-Pesa platform ecosystem—including peer-to-peer transfers, merchant collections, business disbursements, and banking integrations—processes over KES 35 trillion in gross transaction value annually. Concurrently, sector data published by the Communications Authority of Kenya (CA) confirms that Safaricom accounts for over 90% of active mobile money subscriptions nationwide. For any enterprise, e-commerce storefront, or SaaS platform operating in the region, providing an automated checkout flow backed by M-Pesa is the foundational requirement for revenue collection.

While aggregators and third-party gateways offer packaged solutions, direct integration through the Safaricom Daraja API delivers decisive operational advantages:

1. Lower Overhead: Eliminates third-party aggregator processing margins and gateway surcharges in favor of direct Safaricom published merchant tariffs.

2. Instant Settlement: Direct routing into your corporate Paybill or Buy Goods Till, enabling faster settlement into linked commercial bank accounts at institutions like Equity Bank, KCB, or NCBA.

3. Zero Intermediary Latency: Bypasses secondary payment hops, drastically reducing transaction drops caused by third-party gateway downtime during peak shopping hours.

4. Reconciliation Control: Enables granular matching between customer order IDs and official Safaricom receipt numbers (MpesaReceiptNumber).

For modern digital businesses expanding across East Africa, implementing frictionless checkout mechanisms requires dedicated engineering expertise. Aligning your platform with reliable app development in Kenya ensures that your backend architecture handles network retries, currency conversions, and mobile wallet idiosyncrasies reliably and resiliently.

---

Architectural Decisions: Paybill vs Buy Goods Till

Before generating credentials on the Safaricom Developer Portal, you must select the appropriate collection shortcode model. Kenyan businesses operate primarily on two shortcode architectures: Paybill and Buy Goods (Till Number).

+-----------------------------------------------------------------------------------+
|                              M-PESA DARAJA GATEWAY                                |
+-----------------------------------------------------------------------------------+
                                    |
          +-------------------------+-------------------------+
          |                                                   |
          v                                                   v
+-----------------------------+             +----------------------------------+
|           PAYBILL           |             |          BUY GOODS TILL          |
|  (CustomerPayBillOnline)    |             |    (CustomerBuyGoodsOnline)      |
+-----------------------------+             +----------------------------------+
| - Customer enters Shortcode |             | - Customer enters Till Number    |
|   + Account Number          |             | - Zero customer fees at checkout |
| - Best for SaaS, Invoicing, |             | - Best for Retail, Point-of-Sale,|
|   Utilities, B2B Subscriptions|           |   Quick E-Commerce Checkouts     |
| - Configurable fee bands    |             | - Merchant discount rate (MDR)   |
+-----------------------------+             +----------------------------------+

Operational & Fee Comparison

Feature / DimensionPaybill (CustomerPayBillOnline)Buy Goods Till (CustomerBuyGoodsOnline)
Identifier5 to 7-digit Shortcode (e.g., 888555)5 to 7-digit Till Number (e.g., 555123)
Account ReferenceMandatory (Used to distinguish users, invoices, or utility accounts)Optional / Implicit to the Till
Cost AllocationConfigurable: Merchant pays, Customer pays, or Shared fee modelMerchant pays standard MDR; zero transaction fee for customer at checkout
Published Tariff StructureBanded pricing per value tier (e.g., KES 5 to KES 210 depending on transfer band and chosen tariff model, per Safaricom's published Lipa Na M-Pesa merchant schedules)Percentage-based MDR (typically 0.55% of transaction value) subject to a flat fee cap of KES 200 per transaction; collections of KES 200 and below incur zero merchant fee
Reconciliation FitIdeal for ERPs, utility billing, SaaS subscriptions, and recurring invoicesIdeal for instant point-of-sale (POS) and straightforward retail e-commerce checkouts
eTIMS IntegrationStraightforward: maps directly to client KRA PIN and internal invoice IDRequires capturing buyer tax data prior to STK push if a business tax invoice is requested

---

Setting Up the Safaricom Daraja Developer Environment

Safaricom provides a dedicated sandbox environment through the Daraja Developer Portal. Follow these onboarding steps:

1. Create a Developer Account: Visit developer.safaricom.co.ke and complete registration.

2. Create a Sandbox Application:

- Navigate to the My Apps tab.

- Click Create App and supply an application name.

- Check the Lipa Na M-Pesa Online (M-Pesa Express) and Customer to Business (C2B) checkboxes.

- Once generated, copy your Consumer Key and Consumer Secret.

3. Understand Sandbox Mock Credentials:

- Sandbox Shortcode: 174379

- Sandbox Passkey: Provided within the portal simulator documentation.

- Sandbox MSISDN: Use Safaricom test phone numbers (e.g., 254708374149) or configure your verified developer phone number.

4. Network & SSL Requirements:

- Safaricom requires public HTTPS endpoints for all callback URLs.

- Self-signed certificates are rejected; you must maintain a valid TLS/SSL certificate issued by a recognized Certificate Authority (CA).

- In local development, use reverse tunneling tools such as Cloudflare Tunnels or ngrok to expose your local port (e.g., port 3000) over HTTPS.

---

Step 1: Authentication and OAuth 2.0 Token Generation

The Daraja API utilizes OAuth 2.0 Client Credentials grant. You obtain a Bearer token by dispatching a GET request with HTTP Basic Authentication header composed of base64(ConsumerKey:ConsumerSecret).

According to the official Safaricom Daraja API OAuth documentation, access tokens generated via the client credentials grant return an expires_in lifetime of 3,599 seconds (approximately 1 hour). Do not request a new token on every customer checkout. Safaricom enforces strict rate limits on the authentication endpoint; hitting it on every transaction will trigger HTTP 429 throttling. Cache your token in memory or Redis until expiration.

Implementation: Node.js Token Service (`darajaAuth.js`)

import axios from 'axios';

class DarajaAuthService {
  constructor() {
    this.consumerKey = process.env.DARAJA_CONSUMER_KEY;
    this.consumerSecret = process.env.DARAJA_CONSUMER_SECRET;
    this.authUrl = process.env.DARAJA_ENV === 'production'
      ? 'https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials'
      : 'https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials';
    
    this.cachedToken = null;
    this.tokenExpiry = null;
  }

  async getAccessToken() {
    const currentTime = Date.now();
    // Return cached token if valid for at least 60 more seconds
    if (this.cachedToken && this.tokenExpiry && currentTime < (this.tokenExpiry - 60000)) {
      return this.cachedToken;
    }

    const authBuffer = Buffer.from(`${this.consumerKey}:${this.consumerSecret}`).toString('base64');

    try {
      const response = await axios.get(this.authUrl, {
        headers: {
          Authorization: `Basic ${authBuffer}`,
          'Content-Type': 'application/json'
        },
        timeout: 10000
      });

      this.cachedToken = response.data.access_token;
      // expires_in is 3599 seconds per Daraja OAuth specification
      this.tokenExpiry = currentTime + (parseInt(response.data.expires_in, 10) * 1000);
      return this.cachedToken;
    } catch (error) {
      const errorMsg = error.response ? JSON.stringify(error.response.data) : error.message;
      throw new Error(`Daraja OAuth Token generation failed: ${errorMsg}`);
    }
  }
}

export const darajaAuth = new DarajaAuthService();

---

Step 2: Implementing Lipa Na M-Pesa Online (M-Pesa STK Push)

M-Pesa STK Push (also known as M-Pesa Express) initiates a SIM Toolkit prompt directly on the user's mobile device, requesting them to enter their secret M-Pesa PIN.

The Cryptographic Security Hash

Each STK push request requires a dynamic Password calculated as follows:

$$\text{Password} = \text{Base64Encode}(\text{BusinessShortCode} + \text{Passkey} + \text{Timestamp})$$

The Timestamp must strictly follow the format YYYYMMDDHHmmss in East Africa Time (EAT, UTC+3).

Phone Number Normalization

Kenyan phone numbers must be formatted to standard international MSISDN (2547XXXXXXXX or 2541XXXXXXXX). Never pass numbers starting with 07, 01, or +254 directly into the Daraja payload.

function formatKenyanMSISDN(phone) {
  let cleaned = phone.replace(/[^0-9]/g, '');
  if (cleaned.startsWith('0')) {
    cleaned = '254' + cleaned.substring(1);
  } else if (cleaned.startsWith('254')) {
    // Already in international format
  } else if (cleaned.length === 9 && (cleaned.startsWith('7') || cleaned.startsWith('1'))) {
    cleaned = '254' + cleaned;
  }
  
  if (!/^254(7|1)[0-9]{8}$/.test(cleaned)) {
    throw new Error(`Invalid Kenyan mobile number: ${phone}. Must be a valid Safaricom/Airtel line.`);
  }
  return cleaned;
}

Implementation: STK Push Dispatcher (`stkPush.js`)

import axios from 'axios';
import { darajaAuth } from './darajaAuth.js';

export async function initiateStkPush({ amount, phoneNumber, orderId, accountReference }) {
  const shortCode = process.env.DARAJA_SHORTCODE;
  const passkey = process.env.DARAJA_PASSKEY;
  const callbackUrl = process.env.DARAJA_CALLBACK_URL;
  const baseUrl = process.env.DARAJA_ENV === 'production'
    ? 'https://api.safaricom.co.ke'
    : 'https://sandbox.safaricom.co.ke';

  // 1. Generate Timestamp: YYYYMMDDHHmmss in EAT (UTC+3)
  const now = new Date();
  const eatOffsetMs = 3 * 60 * 60 * 1000;
  const eatDate = new Date(now.getTime() + eatOffsetMs);
  const timestamp = eatDate.toISOString().replace(/[^0-9]/g, '').slice(0, 14);

  // 2. Generate Base64 Password
  const rawPassword = `${shortCode}${passkey}${timestamp}`;
  const password = Buffer.from(rawPassword).toString('base64');

  // 3. Normalize Phone Number
  const formattedPhone = formatKenyanMSISDN(phoneNumber);

  // 4. Fetch Active Access Token
  const token = await darajaAuth.getAccessToken();

  const payload = {
    BusinessShortCode: shortCode,
    Password: password,
    Timestamp: timestamp,
    TransactionType: 'CustomerPayBillOnline', // or CustomerBuyGoodsOnline
    Amount: Math.round(amount), // Daraja accepts whole KES integers
    PartyA: formattedPhone,
    PartyB: shortCode,
    PhoneNumber: formattedPhone,
    CallBackURL: callbackUrl,
    AccountReference: accountReference.slice(0, 12), // Max 12 characters
    TransactionDesc: `Pay Order ${orderId}`.slice(0, 13) // Max 13 characters
  };

  try {
    const response = await axios.post(
      `${baseUrl}/mpesa/stkpush/v1/processrequest`,
      payload,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          'Content-Type': 'application/json'
        },
        timeout: 15000
      }
    );

    // Save MerchantRequestID and CheckoutRequestID in DB linked to orderId
    return {
      success: true,
      checkoutRequestId: response.data.CheckoutRequestID,
      merchantRequestId: response.data.MerchantRequestID,
      responseCode: response.data.ResponseCode,
      customerMessage: response.data.CustomerMessage
    };
  } catch (error) {
    const errorDetails = error.response ? error.response.data : error.message;
    return {
      success: false,
      error: errorDetails
    };
  }
}

---

Step 3: Callback Architecture and Webhook Handling

The STK Push API is asynchronous. When your server invokes /processrequest, Safaricom merely confirms that the request was received and queued. The actual transaction execution occurs when the customer enters their PIN. Once processed, Safaricom fires an HTTP POST payload to your designated CallBackURL.

Safaricom Result Codes Reference

Result CodeStatusMeaning & Engineering Action
0SuccessTransaction completed successfully. Update order to PAID.
1032User CancelledThe customer tapped "Cancel" or closed the STK prompt. Mark order CANCELLED.
1Insufficient BalanceThe user's M-Pesa balance cannot cover the payment and Safaricom tariff.
1037TimeoutThe user did not enter a PIN within 60 seconds. Prompt user to retry.
2001Invalid PINCustomer entered an incorrect PIN. Allow re-attempt.

Webhook Security Best Practices

1. Immediate Acknowledgment: Return HTTP 200 with { "ResultCode": 0, "ResultDesc": "Accepted" } within 5 seconds. Never perform blocking tasks (such as sending emails or synchronizing external ERPs) inside the webhook execution loop.

2. Idempotency Guard: Store CheckoutRequestID as a unique database constraint. If Safaricom retries the callback, ensure your application does not double-credit customer balances.

3. IP Range Filtering: Configure your cloud reverse proxy (NGINX, AWS ALB, Cloudflare) to whitelist Safaricom's production IP blocks:

- 196.201.214.0/24

- 196.201.213.0/24

- 196.13.107.0/24

Implementation: Callback Webhook Handler (`webhookServer.js`)

import express from 'express';

const app = express();
app.use(express.json());

app.post('/api/v1/payments/daraja-callback', async (req, res) => {
  // Always immediately acknowledge Safaricom to prevent callback loops
  res.status(200).json({ ResultCode: 0, ResultDesc: 'Callback received successfully' });

  const { Body } = req.body;
  if (!Body || !Body.stkCallback) {
    console.error('Malformed callback payload');
    return;
  }

  const {
    MerchantRequestID,
    CheckoutRequestID,
    ResultCode,
    ResultDesc,
    CallbackMetadata
  } = Body.stkCallback;

  try {
    // 1. Check idempotency: Ensure transaction is pending
    const order = await findOrderByCheckoutRequestId(CheckoutRequestID);
    if (!order) {
      console.warn(`No matching order for CheckoutRequestID: ${CheckoutRequestID}`);
      return;
    }

    if (order.status === 'PAID') {
      console.info(`Order ${order.id} already settled. Skipping duplicate callback.`);
      return;
    }

    // 2. Handle failure conditions
    if (ResultCode !== 0) {
      console.warn(`Payment failed for Order ${order.id}: ${ResultDesc} (Code: ${ResultCode})`);
      await updateOrderStatus(order.id, 'FAILED', { resultCode: ResultCode, reason: ResultDesc });
      return;
    }

    // 3. Extract transaction metadata items
    const metadataItems = CallbackMetadata.Item.reduce((acc, item) => {
      acc[item.Name] = item.Value;
      return acc;
    }, {});

    const amountPaid = metadataItems.Amount;
    const mpesaReceipt = metadataItems.MpesaReceiptNumber;
    const transactionDate = metadataItems.TransactionDate; // Format: 20261105081530
    const customerPhone = metadataItems.PhoneNumber;

    // 4. Validate amount matches original order expectation
    if (amountPaid < order.expectedAmount) {
      await updateOrderStatus(order.id, 'UNDERPAID', { amountPaid, mpesaReceipt });
      return;
    }

    // 5. Update database and trigger downstream events asynchronously
    await updateOrderStatus(order.id, 'PAID', {
      mpesaReceipt,
      amountPaid,
      customerPhone,
      settledAt: new Date()
    });

    // Fire asynchronous background worker for eTIMS receipting and notification
    queuePostPaymentJobs({ orderId: order.id, mpesaReceipt, amountPaid });

  } catch (dbError) {
    console.error(`Error persisting M-Pesa callback for ${CheckoutRequestID}:`, dbError);
  }
});

// Helper stubs
async function findOrderByCheckoutRequestId(id) { /* DB lookup */ return { id: 'ORD-9821', status: 'PENDING', expectedAmount: 4500 }; }
async function updateOrderStatus(id, status, meta) { /* Update record */ }
function queuePostPaymentJobs(data) { /* Dispatch to Redis/RabbitMQ queue */ }

app.listen(3000, () => console.log('Daraja Webhook Server running on port 3000'));

---

Local Regulatory, Tax, and Data Protection Compliance

Integrating payments in Kenya introduces legal, regulatory, and fiscal responsibilities governed by national regulators.

1. Central Bank of Kenya (CBK) National Payment System (NPS) Limits

Under regulatory revisions established by the Central Bank of Kenya for mobile money payment service providers, developers must configure transaction handling logic according to statutory mobile wallet thresholds:

  • Maximum Per-Transaction Limit: KES 250,000 per single mobile money transaction.
  • Maximum Daily Account Limit: KES 500,000 across all wallet movements within a 24-hour cycle.
  • Maximum Account Holding Balance: KES 500,000.

If your checkout total exceeds KES 250,000, your software architecture should either split payments into compliant milestones or offer complementary settlement mechanisms such as PesaLink real-time interbank rails.

2. Office of the Data Protection Commissioner (ODPC) and Data Privacy

Under Kenya's Data Protection Act 2019 and guidance issued by the Office of the Data Protection Commissioner (ODPC), customer mobile phone numbers (MSISDN) and personal identity records transmitted in API callbacks are classified as Personally Identifiable Information (PII). Software teams should apply robust privacy safeguards:

  • Encryption at Rest: Store customer phone numbers, transaction histories, and payment logs in encrypted databases using industry standards such as AES-256.
  • Log Sanitation: Mask sensitive phone numbers in production logging and observability platforms (e.g., logging 2547****3819) to prevent unencrypted PII exposure.
  • Data Minimization: Collect and persist only the payload metadata required for order fulfillment, financial reconciliation, and statutory audit records.

3. Kenya Revenue Authority (KRA) and eTIMS Integration

Commercial enterprises operating in Kenya must adhere to KRA's electronic Tax Invoice Management System (eTIMS) requirements for generating verifiable fiscal electronic tax invoices for commercial sales.

  • Developers should establish an asynchronous worker that receives the confirmed MpesaReceiptNumber from the callback and transmits the invoice payload to KRA's eTIMS API or a certified fiscal integration service.
  • Corporate accounting models should accurately calculate and reconcile applicable statutory excise duty and related taxes levied by telecommunications service providers on financial transaction fees under prevailing Finance Act provisions.

---

Migration to Production: Safaricom Go-Live Protocol

Moving an application from the sandbox to the production gateway (api.safaricom.co.ke) requires an official merchant verification workflow:

1. Corporate KYC Documentation:

To obtain live shortcode production access, Safaricom requires standard corporate verification records:

- Certificate of Incorporation or Business Registration Certificate.

- Company KRA PIN Certificate.

- Official Business Registration Service (BRS) Form CR12 or certified company search confirming current directorship and shareholding structure.

- National Identification Cards (or Passports) of all listed directors.

- Bank Settlement Letter or cancelled cheque verifying the linked corporate bank account.

2. Assigning Web Portal Roles:

Register on the M-Pesa Organization Portal (org.m-pesa.com). Assign the required Business Administrator and Business Operator roles to separate users, fulfilling Safaricom's two-factor separation of duty requirement.

3. Submitting the Go-Live Form:

On the Daraja portal, click Go-Live, upload your KYC pack, select your shortcode type (Paybill or Buy Goods Till), and submit your verified server public IP addresses for whitelisting.

4. Live Verification Test:

Execute a live test with a nominal fee (e.g., KES 10) to confirm end-to-end receipt generation, database persistence, and SMS dispatch before releasing your checkout to public traffic.

---

Frequently Asked Questions (FAQ)

What is the primary difference between C2B and Lipa Na M-Pesa Online (STK Push)?

Lipa Na M-Pesa Online (STK Push) is an inbound-initiated transaction where your website or mobile application prompts the customer's phone to enter their PIN. It is synchronous at initiation and features immediate checkout feedback. Customer-to-Business (C2B), on the other hand, relies on the customer manually opening their SIM toolkit, typing your Paybill number, entering an account reference, and confirming the payment. C2B requires registering Validation and Confirmation URLs on Daraja to listen for passive payments.

Why do STK Push callbacks fail in production when they passed in sandbox?

In sandbox, Safaricom delivers callbacks over standard web ports to any valid URL. In production, Safaricom's firewall strictly enforces SSL certificate validation (TLS 1.2+ required) and rejects any endpoint with intermediate certificate chain issues. Additionally, if your corporate hosting provider blocks incoming POST traffic from Safaricom's IP subnets (196.201.214.0/24), the callback will time out.

How should our backend handle network drops during the STK Push prompt?

If the user does not receive the prompt or the mobile network drops, implement a fallback polling endpoint using the Daraja STK Query API (/mpesa/stkpushquery/v1/query). Query Safaricom after 30 seconds with the CheckoutRequestID to retrieve the definitive state before showing an error message to the customer.

Can an international company integrate the M-Pesa Daraja API?

Yes, through several structured pathways. International enterprises typically integrate the Daraja API by establishing a locally registered Kenyan entity (such as a subsidiary or branch) or by partnering with a licensed local payment aggregator or Merchant of Record (MoR) operating in Kenya. Safaricom also provides enterprise onboarding channels for eligible multinational corporations through its enterprise business division, subject to international Know-Your-Customer (KYC) and Anti-Money Laundering (AML) compliance validation.

---

Sources

Related articles