Skip to content

Repository files navigation

@lumierelabs/daraja

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.

npm license types ci dependenciesstars

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",
});

Table of Contents


Why this exists

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.


Features

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.

Installation

pnpm add @lumierelabs/daraja

Requirements: Node.js 18.17+ (uses the global fetch, AbortController, and btoa/Buffer APIs), TypeScript 5+ recommended.


Quickstart

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 →


Endpoints

Endpoint Method(s) Status
Authentication getAccessToken() auto-caching, handled internally ✅ Working
M-Pesa Express (STK Push) stkPush(), stkPushQuery() ✅ Working
C2B c2b.registerUrl(), c2b.simulate() ⚠️ Upstream Issue
B2C Account Top Up b2cTopUp.topUp() ✅ Working
B2B Hakikisha b2bHakikisha.query() ✅ Working
Dynamic QR dynamicQR.generate() ⚠️ Upstream Issue
Mobile Data Bundles dynamicOffers.fetchOffers(), purchase(), checkStatus() ⚠️ Upstream Issue
SIM Swap swap.check() ✅ Working
IMSI imsi.checkV1(), imsi.checkV2() ✅ Working
Mobile Number Validation mobileNumberValidation.validate() ⚠️ Upstream Issue

⚠️ Upstream Issue means the endpoint is implemented correctly against Safaricom's published spec, but Daraja's own sandbox for that endpoint is inconsistent (thin/rotating test data) that's a Safaricom-side limitation, not a bug in this SDK. See the endpoint's own docs for specifics.

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 →


Framework Integrations

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.


Error Handling

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 →


Configuration

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 →


Roadmap

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.


Documentation

Full documentation every endpoint's exact TypeScript interfaces, framework integration guides, and a dedicated page on Safaricom's own API quirks lives at:

Documentation


Contributing

Contributions are welcome, especially toward the items in Roadmap. Before opening a PR:

  1. Fork the repo and create a branch off main
  2. Run the existing test suite new endpoints should ship with tests, following the pattern in packages/sdk/src/endpoints/
  3. Match the existing validation-and-error-code style (see any existing endpoint client for the pattern request validation throws a scoped INVALID_*_REQUEST error code before any network call)
  4. 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.

Contributors


License

Distributed under the MIT License. Copyright © 2026 Mahito.



Disclaimer: This SDK is an independent open-source project and is not affiliated with, endorsed by, or officially connected to Safaricom PLC. "M-Pesa" and "Daraja" are registered trademarks of Safaricom PLC.

About

A modern TypeScript SDK for the Safaricom Daraja API with beautiful documentation, full type safety, and a developer-first experience.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages