Daraja SDK Logomark
Daraja SDK
Getting Started

Quickstart

Send your first STK Push in under five minutes.

This walks through the single most common thing people reach for the SDK to do: prompting a customer's phone for an M-Pesa PIN entry (STK Push / Lipa na M-Pesa Online).

1. Initialize the client

Daraja can be called with or without new both return the same client, so pick whichever your team's style guide prefers.

daraja.ts
import { Daraja } from "@lumierelabs/daraja";

export const daraja = Daraja({
  consumerKey: process.env.DARAJA_CONSUMER_KEY!,
  consumerSecret: process.env.DARAJA_CONSUMER_SECRET!,
  environment: "sandbox", // 'production' when you go live
});

Daraja(config) throws synchronously if consumerKey or consumerSecret is missing, malformed, or still a placeholder like "YOUR_CONSUMER_KEY" you'll find out at boot time, not three requests deep in production.

2. Send the push

checkout.ts
import { daraja } from "./daraja";

const push = await daraja.stkPush({
  businessShortCode: "174379",
  passkey: process.env.MPESA_PASSKEY!,
  transactionType: "CustomerPayBillOnline",
  amount: 1,
  partyA: "254708374149", // customer's phone (debited)
  partyB: "174379", // your paybill (credited)
  phoneNumber: "254708374149", // where the STK prompt is sent
  callBackURL: "https://example.com/callbacks/stk",
  accountReference: "INV-1042", // shown on the customer's phone, max 12 chars
  transactionDesc: "Order #1042",
});

console.log(push.CheckoutRequestID);
// Save this  you need it to query the transaction status later.

daraja.stkPush(...) is a shortcut for daraja.mpesaExpress.push(...). Both exist so you can either treat STK Push as the SDK's headline feature (top-level) or reach for it alongside the rest of mpesaExpress (namespaced) see M-Pesa Express for the full method.

3. Handle the callback

push() only confirms Daraja accepted the request. The actual outcome (customer entered PIN, cancelled, or timed out) arrives asynchronously as a POST to your callBackURL. Pick your framework:

app/api/callbacks/stk/route.ts
import { NextResponse } from 'next/server';
import type { StkQueryResponse } from '@lumierelabs/daraja';

interface StkCallbackBody {
Body: {
stkCallback: {
MerchantRequestID: string;
CheckoutRequestID: string;
ResultCode: number;
ResultDesc: string;
CallbackMetadata?: {
Item: Array<{ Name: string; Value?: string | number }>;
};
};
};
}

export async function POST(request: Request) {
  const payload = (await request.json()) as StkCallbackBody;
  const { ResultCode, ResultDesc, CheckoutRequestID } = payload.Body.stkCallback;

if (ResultCode === 0) {
const items = payload.Body.stkCallback.CallbackMetadata?.Item ?? [];
const amount = items.find((item) => item.Name === 'Amount')?.Value;
const receipt = items.find((item) => item.Name === 'MpesaReceiptNumber')?.Value;
console.log(`Payment confirmed: ${amount} KES, receipt ${receipt}`);
} else {
console.log(`Payment failed for ${CheckoutRequestID}: ${ResultDesc}`);
}

// Daraja only cares that you returned 200 it does not read this body.
return NextResponse.json({ ResultCode: 0, ResultDesc: 'Accepted' });
}

Local development

Daraja needs a publicly reachable URL for callBackURL localhost will never receive anything. Use a tunnel (Cloudflare Tunnel, or ngrok for local testing only) and swap it for a real domain before you register anything in production. See Safaricom API Quirks for why ngrok specifically causes problems later.

4. (Optional) Query the status directly

If your callback never arrives dropped webhook, firewall, whatever poll instead:

const status = await daraja.stkPushQuery({
  businessShortCode: '174379',
  passkey: process.env.MPESA_PASSKEY!,
  checkoutRequestId: push.CheckoutRequestID,
});

console.log(status.ResultDesc);

That's the whole loop. From here:

On this page