A fully typed TypeScript client for Safaricom's Daraja (M-Pesa) API.
Auto-caching OAuth tokens. Timezone-correct STK passwords, generated for you. Safaricom's own field misspellings, normalized behind a clean interface not papered over.
Documentation · Quickstart · Report a Bug · Request a Feature
import { Daraja } from "@lumierelabs/daraja";
const daraja = Daraja({
consumerKey: process.env.DARAJA_CONSUMER_KEY!,
consumerSecret: process.env.DARAJA_CONSUMER_SECRET!,
shortcode: process.env.DARAJA_SHORTCODE!,
passkey: process.env.DARAJA_PASSKEY!,
callbackUrl: process.env.DARAJA_CALLBACK_URL!,
environment: "sandbox",
});
const { CheckoutRequestID } = await daraja.stkPush({
transactionType: "CustomerPayBillOnline",
amount: 1,
partyA: "254708374149",
phoneNumber: "254708374149",
accountReference: "INV-1042",
});- Why this exists
- Features
- Installation
- Quickstart
- Endpoints
- Framework Integrations
- Error Handling
- Configuration
- Roadmap
- Documentation
- Contributing
- License
Daraja's own documentation is functional but thin. Field names change case between endpoints OriginatorCoversationID and RecieverIdentifierType are real, misspelled fields Safaricom ships in production, not typos in this SDK. The STK Push password has to be hand-built as Base64(ShortCode + Passkey + Timestamp) in the exact East Africa timezone, regardless of where your server runs. And half the integration "gotchas" IP whitelisting vs. local tunnels, callback URLs silently rejected for containing the word mpesa, sandbox instability with no status page are only discoverable by hitting them in production.
@lumierelabs/daraja bakes those lessons in, so you don't have to relearn them from a failed sandbox call at 11pm.
| Fully typed payloads | Every request and response shape typed end to end, including the fields Safaricom itself misspells on the wire. |
| Automatic token lifecycle | OAuth 2.0 tokens fetched, cached in memory, and refreshed automatically with a 60-second safety buffer. You never call /oauth/v1/generate yourself. |
| Daraja quirks, normalized | Timezone-correct STK passwords, callback URL validation before a request ever leaves your server, documented sandbox instability instead of a black box. |
| One error shape | Every failure client-side validation or a Daraja-side rejection throws a single DarajaError class. One catch, everywhere. |
| Multi-framework native | Built on native fetch and AbortController, no runtime-specific glue. First-class integration guides for Next.js, Astro, Remix, and Express. |
| Zero heavy dependencies | No axios, no node-fetch, no isomorphic-fetch. Ships as both ESM and CJS with a single .d.ts. |
pnpm add @lumierelabs/darajaRequirements: Node.js 18.17+ (uses the global fetch, AbortController, and btoa/Buffer APIs), TypeScript 5+ recommended.
import { Daraja } from "@lumierelabs/daraja";
export const daraja = Daraja({
consumerKey: process.env.DARAJA_CONSUMER_KEY!,
consumerSecret: process.env.DARAJA_CONSUMER_SECRET!,
shortcode: process.env.DARAJA_SHORTCODE!,
passkey: process.env.DARAJA_PASSKEY!,
callbackUrl: process.env.DARAJA_CALLBACK_URL!,
environment: "sandbox", // 'production' when you go live
});
const push = await daraja.stkPush({
transactionType: "CustomerPayBillOnline",
amount: 1,
partyA: "254708374149", // customer's phone (debited)
phoneNumber: "254708374149", // where the STK prompt is sent
accountReference: "INV-1042",
});
console.log(push.CheckoutRequestID);Daraja(config) validates your credentials synchronously malformed or placeholder keys throw immediately, before any request touches the network.
Full walkthrough, including callback handling per framework: Quickstart Guide →
| Endpoint | Method(s) | Status |
|---|---|---|
| Authentication | getAccessToken() auto-caching, handled internally |
✅ Working |
| M-Pesa Express (STK Push) | stkPush(), stkPushQuery() |
✅ Working |
| C2B | c2b.registerUrl(), c2b.simulate() |
|
| B2C Account Top Up | b2cTopUp.topUp() |
✅ Working |
| B2B Hakikisha | b2bHakikisha.query() |
✅ Working |
| Dynamic QR | dynamicQR.generate() |
|
| Mobile Data Bundles | dynamicOffers.fetchOffers(), purchase(), checkStatus() |
|
| SIM Swap | swap.check() |
✅ Working |
| IMSI | imsi.checkV1(), imsi.checkV2() |
✅ Working |
| Mobile Number Validation | mobileNumberValidation.validate() |
Looking for Account Balance, Transaction Status, Reversal, or a generic B2C/B2B Payment Request? They aren't implemented yet see Roadmap for the honest list instead of documentation for endpoints that would 404.
Full request/response types, validation rules, and Safaricom-specific quirks for every endpoint: Endpoint Documentation →
First-class, copy-pasteable guides for wiring @lumierelabs/daraja (including webhook/callback handlers) into:
| Next.js App Router |
Astro SSR |
Remix React Router v7 |
Express Node.js |
Since the SDK is built entirely on native fetch, it isn't limited to these four they're just the ones with dedicated written guides today.
Every failure a validation error caught before a request is sent, a rejected response from Daraja, a network timeout is thrown as a single DarajaError:
import { Daraja, DarajaError } from "@lumierelabs/daraja";
try {
await daraja.stkPush({/* ... */});
} catch (error) {
if (error instanceof DarajaError) {
console.error(error.errorCode, error.message);
if (error.suggestion) console.error("Suggestion:", error.suggestion);
} else {
throw error;
}
}class DarajaError extends Error {
readonly statusCode: number; // HTTP status, or 0 for client-side validation errors
readonly errorCode: string; // e.g. "INVALID_STK_REQUEST"
readonly endpoint?: string;
readonly suggestion?: string; // a human-readable fix, when the SDK knows one
readonly rawResponse?: unknown; // Daraja's raw JSON body
}statusCode === 0 means the SDK caught a bad request shape before it ever left your machine. statusCode > 0 means Daraja itself rejected the call. Full error code reference: Error Handling →
interface DarajaConfig {
consumerKey: string;
consumerSecret: string;
shortcode?: string; // default shortcode for STK Push/query
passkey?: string; // default Lipa Na M-Pesa Online passkey
callbackUrl?: string; // default callback URL for STK Push
environment?: "sandbox" | "production"; // default: 'sandbox'
timeout?: number; // default: 10000 (ms)
}| Option | Type | Default | Description |
|---|---|---|---|
consumerKey |
string |
— | Required. Exactly 48 alphanumeric characters. |
consumerSecret |
string |
— | Required. Exactly 64 alphanumeric characters. |
shortcode |
string |
"" |
Default shortcode for M-Pesa Express STK Push and query requests. |
passkey |
string |
"" |
Default Lipa Na M-Pesa Online passkey for STK Push and query requests. |
callbackUrl |
string |
"" |
Default callback URL for STK Push requests. |
environment |
'sandbox' | 'production' |
'sandbox' |
'production' requires HTTPS on every callback URL. |
timeout |
number |
10000 |
Milliseconds before a request is aborted via AbortController. |
Full validation pipeline (placeholder detection, key/secret swap detection, sanitization): Configuration →
Not implemented in the current version tracked honestly rather than left undocumented:
- General B2C Payment Request (disbursing to an arbitrary customer MSISDN distinct from B2C Account Top Up, which moves funds between your own accounts)
- General B2B Payment Request (distinct from B2B Hakikisha, which is a name/tariff lookup only)
- Account Balance query
- Transaction Status query
- Reversal API
Contributions closing any of these are very welcome see Contributing.
Full documentation every endpoint's exact TypeScript interfaces, framework integration guides, and a dedicated page on Safaricom's own API quirks lives at:
Contributions are welcome, especially toward the items in Roadmap. Before opening a PR:
- Fork the repo and create a branch off
main - Run the existing test suite new endpoints should ship with tests, following the pattern in
packages/sdk/src/endpoints/ - Match the existing validation-and-error-code style (see any existing endpoint client for the pattern request validation throws a scoped
INVALID_*_REQUESTerror code before any network call) - Open a PR describing what changed and why
For bugs or feature requests, please open an issue rather than a PR first, so the approach can be discussed.
Distributed under the MIT License. Copyright © 2026 Mahito.