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.
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
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:
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:
- Authentication how token caching actually works under the hood
- M-Pesa Express the full request/response shape
- Framework Integrations production-shaped route handlers