Web DevelopmentADVANCED 22 min read

Payment Gateway Integrations: Safaricom Daraja 3.0 M-Pesa STK Push & PayHero Kenya API

End-to-end payment integration manual for Safaricom Daraja 3.0 (Lipa Na M-Pesa Online STK push, OAuth 2.0 token generation, password calculation, webhook callback handling, transaction status query) and PayHero Kenya API (unified channel tokens, automated STK push, and IPN HMAC signature validation).

Standard Remediation Protocol

7 verified steps
01

Step 1

Generate Safaricom Daraja 3.0 OAuth 2.0 bearer access token using Base64 encoded consumer credentials.

02

Step 2

Calculate timestamped password hash (Base64 of Shortcode, Passkey, and YYYYMMDDHHmmss timestamp).

03

Step 3

Dispatch Lipa Na M-Pesa Online STK Push requests to customer mobile devices via the Daraja 3.0 processrequest endpoint.

04

Step 4

Deploy an asynchronous HTTPS webhook callback endpoint to ingest and validate payment confirmation payloads.

05

Step 5

Query Daraja 3.0 transaction status endpoint to resolve edge cases where callbacks time out or network drops.

06

Step 6

Integrate PayHero Kenya API for multi-channel collections and verify IPN webhook signatures with HMAC hashes.

07

Step 7

Enforce database transaction idempotency to prevent duplicate fulfillment from replayed webhook requests.

Multi-OS Terminal Diagnostic Commands

PowerShell • Windows CMD • Bash
Invoke-RestMethod -Uri "https://sandbox.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials" -Headers @{Authorization="Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes('$KEY:$SECRET')))"}

Production Verification Checklist

Interactive Diagnostic Checklist

Check off steps as you execute troubleshooting workflows. Saved locally in your browser.

0/8 (0%)
Register developer account on Safaricom Developer Portal and create a Daraja 3.0 application.
Obtain Consumer Key, Consumer Secret, Passkey, and BusinessShortCode for sandbox testing.
Implement OAuth 2.0 bearer token fetch with 58-minute in-memory caching.
Compute timestamped security password (Base64 of Shortcode + Passkey + Timestamp).
Build STK push endpoint with phone number normalization to 254XXXXXXXXX format.
Deploy public HTTPS callback endpoint and verify connectivity using cloudflared tunnel or ngrok.
Implement database idempotency check to verify receipt uniqueness before fulfilling customer orders.
Configure PayHero Kenya channel credentials and test fallback payment routing.

Architectural Details & Remediation Manual

Production Payment Integration: Safaricom Daraja 3.0 & PayHero Kenya API

In East Africa, M-Pesa is the primary settlement medium for digital commerce. Integrating payment infrastructure requires understanding asynchronous telco webhooks, cryptographic password calculations, OAuth bearer tokens, and bulletproof idempotency controls to prevent lost payments or double-crediting.

1. Safaricom Daraja 3.0 STK Push Lifecycle

Lipa Na M-Pesa Online (STK Push) triggers an on-screen PIN prompt directly on the customer's phone:

  1. Customer Checkout: User enters mobile number (e.g., 0712345678 or 254712345678) and submits payment order.
  2. Backend Server: Obtains OAuth token from Safaricom, computes the security password hash, and dispatches an STK Push request to Daraja API.
  3. Safaricom Core Gateway: Pushes prompt to SIM toolkit on customer handset. Customer enters M-Pesa PIN.
  4. Customer Handset: Validates PIN and executes fund debit. Safaricom returns SMS receipt to customer.
  5. Asynchronous Webhook Callback: Safaricom dispatches an HTTP POST request containing full transaction metadata to your configured CallBackURL.
  6. Backend Reconciliation: Backend parses callback payload, validates ResultCode === 0, checks receipt number against database, and credits user order.

2. Daraja 3.0 Implementation Code

// lib/daraja.ts
export async function getDarajaToken(): Promise {
  const consumerKey = process.env.MPESA_CONSUMER_KEY!;
  const consumerSecret = process.env.MPESA_CONSUMER_SECRET!;
  const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64');

  const res = await fetch('https://api.safaricom.co.ke/oauth/v1/generate?grant_type=client_credentials', {
    headers: { Authorization: `Basic ${auth}` },
    next: { revalidate: 3500 } // Cache token for 58 minutes
  });

  const data = await res.json();
  if (!res.ok) throw new Error('Failed to get Daraja OAuth token');
  return data.access_token;
}

export async function triggerStkPush(params: {
  phoneNumber: string;
  amount: number;
  orderId: string;
  callbackUrl: string;
}) {
  const token = await getDarajaToken();
  const shortcode = process.env.MPESA_SHORTCODE!;
  const passkey = process.env.MPESA_PASSKEY!;
  
  // Format timestamp: YYYYMMDDHHmmss
  const now = new Date();
  const timestamp = now.toISOString().replace(/[^0-9]/g, '').slice(0, 14);
  const password = Buffer.from(`${shortcode}${passkey}${timestamp}`).toString('base64');

  // Format phone number to 254XXXXXXXXX
  let phone = params.phoneNumber.replace(/\\D/g, '');
  if (phone.startsWith('0')) phone = '254' + phone.slice(1);
  if (phone.startsWith('+')) phone = phone.slice(1);

  const payload = {
    BusinessShortCode: shortcode,
    Password: password,
    Timestamp: timestamp,
    TransactionType: "CustomerPayBillOnline", // or CustomerBuyGoodsOnline
    Amount: Math.round(params.amount),
    PartyA: phone,
    PartyB: shortcode,
    PhoneNumber: phone,
    CallBackURL: params.callbackUrl,
    AccountReference: params.orderId,
    TransactionDesc: "Portal Subscription Payment"
  };

  const res = await fetch('https://api.safaricom.co.ke/mpesa/stkpush/v1/processrequest', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(payload)
  });

  const responseData = await res.json();
  return responseData; // contains CheckoutRequestID, ResponseCode
}

3. Asynchronous Webhook Callback Route with Idempotency

// app/api/v1/mpesa/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';

export async function POST(req: NextRequest) {
  try {
    const data = await req.json();
    const { Body } = data;
    const { stkCallback } = Body;

    const resultCode = stkCallback.ResultCode;
    const resultDesc = stkCallback.ResultDesc;
    const checkoutRequestId = stkCallback.CheckoutRequestID;

    // ResultCode 0 indicates payment success
    if (resultCode === 0) {
      const items = stkCallback.CallbackMetadata.Item;
      const receipt = items.find((i: any) => i.Name === 'MpesaReceiptNumber')?.Value;
      const amount = items.find((i: any) => i.Name === 'Amount')?.Value;
      const phone = items.find((i: any) => i.Name === 'PhoneNumber')?.Value;
      const txDate = items.find((i: any) => i.Name === 'TransactionDate')?.Value;

      // Idempotency: Check if transaction has already been fulfilled
      const existingTx = await db.payment.findUnique({
        where: { mpesaReceipt: receipt }
      });

      if (existingTx) {
        // Acknowledge receipt without double crediting
        return NextResponse.json({ ResultCode: 0, ResultDesc: "Accepted duplicate" });
      }

      // Record payment and update order
      await db.$transaction([
        db.payment.create({
          data: {
            checkoutRequestId,
            mpesaReceipt: receipt,
            amount: Number(amount),
            phoneNumber: String(phone),
            status: "COMPLETED",
            rawPayload: JSON.stringify(data)
          }
        }),
        db.order.updateMany({
          where: { checkoutRequestId },
          data: { status: "PAID", receiptNumber: receipt }
        })
      ]);
    } else {
      // Payment was cancelled by user (1032) or failed
      await db.order.updateMany({
        where: { checkoutRequestId },
        data: { status: "FAILED", failureReason: resultDesc }
      });
    }

    // Always respond with HTTP 200 to Safaricom
    return NextResponse.json({ ResultCode: 0, ResultDesc: "Callback processed successfully" });
  } catch (error) {
    console.error("M-Pesa Callback Error:", error);
    return NextResponse.json({ ResultCode: 0, ResultDesc: "Callback acknowledged with internal log" });
  }
}

4. PayHero Kenya API: Unified Multi-Channel Gateway

PayHero provides aggregated settlement across Till numbers, Paybills, and mobile wallets with automated webhook callbacks.

// Initiating PayHero Express STK Push
export async function createPayHeroPayment(params: {
  amount: number;
  phone: string;
  orderReference: string;
}) {
  const authHeader = 'Basic ' + Buffer.from(
    `${process.env.PAYHERO_API_KEY}:${process.env.PAYHERO_API_SECRET}`
  ).toString('base64');

  const res = await fetch('https://backend.payhero.co.ke/api/v2/payments', {
    method: 'POST',
    headers: {
      Authorization: authHeader,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      amount: params.amount,
      phone_number: params.phone,
      channel_id: parseInt(process.env.PAYHERO_CHANNEL_ID!),
      provider: 'm-pesa',
      external_reference: params.orderReference,
      callback_url: 'https://api.cybertechcomps.com/api/v1/payhero/webhook'
    })
  });

  return await res.json();
}
Tags:#Payments#M-Pesa#Daraja 3.0#STK Push#PayHero#Webhooks#Idempotency#Fintech

Was this troubleshooting guide helpful?

Your verification helps maintain remediation accuracy across technical guides.

Community Troubleshooting & Insights (0)