npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kepas/daraja-js

v1.4.0

Published

Type-safe Node.js/TypeScript SDK for Safaricom Daraja (M-Pesa). Encodes the production gotchas that silently break real PayBills.

Readme

daraja-js

Type-safe Node.js / TypeScript SDK for Safaricom Daraja (M-Pesa). It encodes the production gotchas that silently break real PayBills — so you don't rediscover them in your own outage.

npm CI License Types

Stable (1.x). The public API follows SemVer — breaking changes bump the major. Live-tested against production M-Pesa. See the roadmap for what's next.

Not affiliated with or endorsed by Safaricom PLC. "Daraja" is the public name of Safaricom's M-Pesa Open API; this is an independent, community toolkit. See TRADEMARK.md.


Why this exists

The Daraja API works. The problem is the dozen undocumented behaviors that pass in sandbox and fail in production — phone numbers that must be JSON numbers, balances pipe-delimited into a single string, callbacks that never retry. Every team that ships on M-Pesa rediscovers these the hard way, usually mid-incident.

daraja-js is the distillation of a production PayBill (Safaricom shortcode 4052037) into a typed SDK. Each gotcha below is a class of bug we hit in production and now prevent at the type level or in the request layer.

Install

npm install @kepas/daraja-js
# or
pnpm add @kepas/daraja-js

Node 20+. Ships ESM + CJS + types. Works in Node, Bun, and Cloudflare Workers (WebCrypto-backed webhook verification).

Before you go live, read PREREQUISITES.md. It lists — per API — the exact Safaricom credentials, the Daraja product you must enable on your app, and the operator role / Go-Live each capability requires. The code is ready; Safaricom controls access, and a missing product returns 401 "no apiproduct match found".

Quickstart

ESM

import { Daraja, DarajaInsufficientFundsError } from '@kepas/daraja-js';

const daraja = new Daraja({
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
  shortcode: '600999',                // your own shortcode (600999 = Safaricom sandbox)
  passkey: process.env.MPESA_PASSKEY!,
  environment: 'sandbox',             // 'sandbox' | 'production'
  maxNetworkRetries: 2,
  // Required for the initiator-authed APIs (b2c.*, b2b.*, status.transaction, reversal):
  // initiator: process.env.MPESA_INITIATOR!,
  // securityCredential: await generateSecurityCredential({ … }),  // RSA-encrypt the initiator password
  // Required for Bill Manager invoicing (the key billManager.optIn returns):
  // billManagerAppKey: process.env.MPESA_BILLMANAGER_APP_KEY!,
});

const res = await daraja.collect.stkPush({
  phone: '0712345678',                // any of 5 formats — normalized internally
  amount: 100,
  accountReference: 'INV-001',
  description: 'Subscription payment',
  callbackUrl: 'https://pay.example.com/webhooks/mpesa/stk',
});

console.log(res.checkoutRequestId);

CJS

const { Daraja } = require('@kepas/daraja-js');

Sharing the OAuth token across workers (Redis)

By default the token is cached per-process. To share one token across many workers, pass a tokenStore — two functions over any backend (the SDK has no Redis dependency):

import Redis from 'ioredis';
const redis = new Redis();

const daraja = new Daraja({
  /* …creds… */
  tokenStore: {
    get: (key) => redis.get(key),
    set: (key, value, ttlSeconds) => redis.set(key, value, 'EX', ttlSeconds).then(() => undefined),
  },
});

The in-memory fast path still applies — Redis is only read when the local token is cold. Keys are namespaced per environment + consumer key.

Receiving the STK result (Daraja callback)

Safaricom POSTs the async result to your callbackUrl. Daraja does not sign it, so pair this with an IP allowlist for Safaricom's ranges.

import { parseStkCallback } from '@kepas/daraja-js';

app.post('/webhooks/mpesa/stk', express.json(), (req, res) => {
  const result = parseStkCallback(req.body);
  if (result.success) {
    // result.mpesaReceiptNumber, result.amount, result.phoneNumber
  }
  res.status(200).end(); // ALWAYS 200 to Safaricom
});

C2B — direct PayBill/Till payments

Register your callback URLs once, then handle the validation + confirmation callbacks. The confirmation is terminal — money is already settled and Safaricom won't retry it, so always reply 200.

import { parseC2bConfirmation, c2bAccept, c2bReject } from '@kepas/daraja-js';

// one-time setup
await daraja.c2b.registerUrls({
  confirmationUrl: 'https://example.com/c2b/confirm',
  validationUrl: 'https://example.com/c2b/validate',
});

// validation (optional): accept or reject before the payment completes
app.post('/c2b/validate', express.json(), (req, res) => res.json(c2bAccept()));

// confirmation: money is in — record it, always 200
app.post('/c2b/confirm', express.json(), (req, res) => {
  const p = parseC2bConfirmation(req.body); // p.transId, p.amount, p.msisdn, p.billRefNumber
  res.status(200).end();
});

B2C — pay out to a customer phone

Money out. Needs initiator auth — set initiator + securityCredential on the client. B2C draws from your Utility account (gotcha #7), so fund it first.

import { Daraja, generateSecurityCredential, parseB2cResult } from '@kepas/daraja-js';

const daraja = new Daraja({
  consumerKey, consumerSecret, shortcode: '600999', passkey, environment: 'sandbox',
  initiator: 'apitest',
  // one-time: RSA-encrypt your initiator password with Safaricom's cert
  securityCredential: generateSecurityCredential({ password, certPath: './certs/sandbox.cer' }),
});

const ack = await daraja.b2c.send({
  phone: '0712345678',
  amount: 500,
  resultUrl: 'https://example.com/b2c/result',
  queueTimeoutUrl: 'https://example.com/b2c/timeout',
  remarks: 'Refund',
});

// async result lands at resultUrl:
app.post('/b2c/result', express.json(), (req, res) => {
  const r = parseB2cResult(req.body); // r.success, r.mpesaReceipt, r.amount, r.recipientName
  res.status(200).end();
});

Re-emitting signed webhooks (Stripe-compatible)

For platforms built on daraja-js that forward events to their own consumers — sign on send, verify on receive. Works on Node and edge runtimes.

import { webhooks } from '@kepas/daraja-js';

app.post('/webhooks/mpesa/stk',
  express.raw({ type: 'application/json' }),   // RAW bytes — not parsed JSON
  async (req, res) => {
    const event = await webhooks.constructEventAsync({
      payload: req.body,
      signature: req.headers['x-daraja-signature'] as string,
      secret: process.env.MPESA_WEBHOOK_SECRET!,
    });
    // ... handle event ...
    res.status(200).end();                      // ALWAYS 200 to Safaricom
  }
);

The gotchas it defeats

These are real production failures, encoded so you never meet them:

| # | Gotcha | How the SDK handles it | |---|--------|------------------------| | 1 | STK PartyA/PhoneNumber must be JSON numbers — strings silently time out (ResultCode 1037) | Cast after normalization, enforced by the request type | | 2 | Phone numbers arrive in many formats — 07XX and the newer 01XX, plus +254…, 254…, bare 9-digit, and a hashed (SHA-256 hex) MSISDN | normalizePhone(), tested across all ranges | | 3 | Timestamp is YYYYMMDDHHMMSS UTC, zero-padded | Generated internally | | 4 | STK password = base64(shortcode + passkey + timestamp), order matters | Derived for you | | 5 | B2B callback URL is shared between float transfers and B2B payments | Typed callback parsers + optional router helper | | 6 | Balance is pipe-delimited, accounts &-joined | parseBalance() returns a typed struct | | 7 | B2C draws from the Utility account, not Working | Config-time warning when float is untopped | | 8 | C2B confirmation is terminal — no second callback | Parser sets terminal: true | | 9 | Safaricom does not retry C2B callbacks | Pull Transaction recovery cookbook | | 10 | Pull Transaction 3.0: no /mpesa/ prefix, NominatedNumber is MSISDN not shortcode, OffSetValue is a number | Correct paths + typed params | | 11 | Always return 200 to callbacks, even on bad payloads | Helper returns 200 + persists for replay | | 12 | OAuth token TTL 3599s | Cached per-environment, race-safe | | 13 | Prod vs sandbox base URLs | Single environment flag | | 14 | Bank withdrawal is not API-automatable | Documented; no misleading stub |

Two more the SDK exposes as helpers: amounts ≤100 KES on B2B PayBill are free-tier (no assumed fee), and reversal-failure-by-recipient-spend (isSettledByRecipientSpend()) since Safaricom signals it only via free-text resultDesc.

What's covered

Collection (money in):

  • collect.stkPush + parseStkCallback — STK Push, with the gotcha-defeating validation layer; parse the async result Safaricom posts back.
  • c2b.registerUrls + parseC2bConfirmation / parseC2bValidation + c2bAccept / c2bReject — capture direct PayBill/Till payments (confirmation is terminal — gotcha #8).
  • bonga.calculatePoints + bonga.redeem — Lipa na Bonga: convert Bonga points→KES and redeem them as payment (settles via the C2B confirmation callback).

Disbursement (money out):

  • b2c.send + parseB2cResult — disburse to a customer phone (Utility account — gotcha #7).
  • b2c.toPochi — pay a customer's business wallet (pochi la biashara), BusinessPayToPochi.
  • b2b.pay + b2b.transferFloat + parseB2bResult — pay another business, and move float Working↔Utility (funds B2C).
  • b2b.topUp — B2C Account Top Up (BusinessPayToBulk): load a B2C shortcode's Utility account.
  • b2b.remitTax — Tax Remittance to KRA (PayTaxToKRA).

Standing orders & express:

  • ratiba.create + parseRatibaCallback — M-Pesa Ratiba: create a customer standing order (recurring collection).
  • express.checkout + parseExpressCallback — B2B Express Checkout: USSD push to a merchant's till to pay a vendor paybill.

Account management & reconciliation:

  • status.stkPush (sync) + status.transaction (async) + parseStatusResult — query a transaction's outcome.
  • reversal.request + parseReversalResult + isSettledByRecipientSpend — reverse a transaction; classify the "recipient already spent it" case (gotcha #16).
  • balance.query + parseBalanceResult / parseAccountBalance — query account balances, with the pipe-delimited parser (gotcha #6).
  • pull.registerUrl + pull.query — Pull Transaction API (Daraja 3.0) to backfill C2B payments missed when a callback failed (gotcha #10).
  • orgInfo.query — validate a shortcode's name + tariff before paying (synchronous; reduces reversals to the wrong till/paybill).
  • qr.generate — dynamic QR codes (Pay Bill / Buy Goods / Send Money / etc.).

Invoicing:

  • billManager.optIn / updateOptIn / sendInvoice / sendBulkInvoices / cancelInvoice / cancelBulkInvoices / acknowledgePayment + parseBillManagerPayment + billManagerAck — Bill Manager invoicing & reconciliation.

Cross-cutting:

  • generateSecurityCredential — RSA-encrypt the initiator password for the initiator-authed APIs.
  • webhooks.sign / constructEvent / constructEventAsync — Stripe-compatible signing + verification (sync + edge).
  • The phone / amount / timestamp / password primitives (normalizePhone, phoneToNumber, makeTimestamp, generatePassword, validateAmount).
  • The DarajaError hierarchy + errorFromResult / errorFromResponse + a proven result-code catalog that turns Safaricom's codes into actionable messages — see ERROR_CODES.md.
  • OAuth token management (race-safe, 3599s TTL) and the HTTP transport.
  • Pluggable cross-process token cache (tokenStore) — share one OAuth token across workers (e.g. Redis), no SDK Redis dependency.

This covers 100% of Safaricom's money APIs — collection, disbursement, standing orders, account management, reconciliation, invoicing, and QR. (Out of scope by design: the sandbox-only C2B simulate endpoint and the non-money telco APIs.) The commercial/add-on products (Bill Manager, Ratiba, B2B Express, Query Org Info, Lipa na Bonga) require enabling on your Daraja app — and some a Go-Live — before live calls succeed.

Full surface in the API reference (auto-published from each commit to main).

TypeScript

Types are bundled — no @types/daraja-js needed. Inputs and Daraja callbacks are fully typed; the error hierarchy (DarajaErrorDarajaAuthError, DarajaInsufficientFundsError, …) lets you branch on recoverable vs fatal.

Proven meaningful errors

Safaricom returns terse codes (1037, SFC_IC0003, …) and publishes no complete code reference. daraja-js ships a catalog where each meaning traces to a source we can point to — a real production response we observed, our own code, or official Safaricom docs. Parsed results carry the extras:

const r = parseStkCallback(req.body);
if (!r.success) {
  console.log(r.resultCode);  // 1037
  console.log(r.resultDesc);  // Safaricom's verbatim text (never altered)
  console.log(r.meaning);     // "The customer didn't respond to the STK prompt within ~60s…"
  console.log(r.retriable);   // true
  console.log(r.catalogued);  // true; if false, the code isn't catalogued and we pass Safaricom's text through unchanged
}

A code we can't prove is passed through verbatim (catalogued: false) — we never fabricate a meaning. Full table + proof sources: docs/ERROR_CODES.md.

Security

Webhook signatures use the Stripe-compatible scheme (t=…,v1=…, HMAC-SHA256 over raw body, constant-time compare, replay window). Report vulnerabilities per SECURITY.mdnot via public issues. We ship neither Safaricom certificate (they own those); generateSecurityCredential() works against your own.

Three things you own when integrating:

  • Verify the raw body. Pass the exact request bytes to webhooks.constructEvent(Async) — not JSON.stringify(req.body), which re-serializes and breaks the signature.
  • Make callback handlers idempotent. The SDK verifies signatures but keeps no replay cache; dedupe on the transaction/receipt id so a re-delivered (or replayed-within-window) callback isn't processed twice.
  • Don't blindly log errors. DarajaError.raw (the Daraja response, which may contain MSISDN/receipts) is non-enumerable, so JSON.stringify/console.log/most logger serializers skip it — but if you explicitly read err.raw, scrub PII before logging it.

Telemetry

Off by default. The SDK makes no network calls except to Safaricom.

Support this project 💛

daraja-js is free and Apache 2.0. If it saved you an outage, you can fund its maintenance via M-Pesa — the same rail this SDK is built on:

Pay Bill → Business no. 4052037 → Account no. daraja → amount → PIN.

Donations are voluntary and buy no support guarantees or roadmap influence. International card rails and full details in DONATING.md.

Community

License

Apache License 2.0. Patent grant included.