The Daraja (M-Pesa) SDK that turns ~800 lines of plumbing into ~8.
Tiny, fully typed, runtime-agnostic client for Safaricom's Daraja API. Automatic OAuth, decoded errors, and a callback parser that flattens M-Pesa's ugliest payload for you.
Every M-Pesa integration re-implements the same boilerplate: base64 the credentials, hit
the OAuth endpoint, cache the token, format an EAT timestamp, build the Password, get the
BusinessShortCode vs ShortCode field names right, normalize phone numbers, and then
decode a nested CallbackMetadata.Item[] array where 2001 cryptically means "wrong PIN".
That's hundreds of lines before you take a single shilling. This SDK is that, done once, tested, and typed:
import { Daraja } from "@paylod/daraja";
// Credentials come from the environment — never hard-code them.
const daraja = new Daraja({
consumerKey: process.env.DARAJA_CONSUMER_KEY!,
consumerSecret: process.env.DARAJA_CONSUMER_SECRET!,
shortcode: process.env.DARAJA_SHORTCODE,
passkey: process.env.DARAJA_PASSKEY,
environment: "sandbox",
});
const res = await daraja.stkPush({
amount: 10,
phoneNumber: "2547...", // 07.., +254.., or 254.. — all normalized for you
accountReference: "INV-1",
callbackUrl: "https://your.app/callback",
});
console.log(res.CheckoutRequestID); // 👈 you're taking paymentsNo manual token fetching. No timestamp math. No field-name landmines.
npm i @paylod/daraja
# or: pnpm add @paylod/daraja · yarn add @paylod/daraja · bun add @paylod/darajaRequires Node 18+ (or Deno / Bun / an edge runtime). Zero runtime dependencies.
| API | Method | Status |
|---|---|---|
| OAuth token (auto) | (internal — never call it) | ✅ |
| STK Push | stkPush() |
✅ |
| STK Query | stkQuery() |
✅ |
| Dynamic QR | generateQr() |
✅ |
| C2B — Register URL | registerC2BUrls() |
✅ |
| C2B — Simulate (sandbox) | simulateC2B() |
✅ |
| STK callback parser | parseStkCallback() |
✅ |
| Transaction Status | buildTransactionStatusBody() (builder only) |
🔜 |
| Account Balance | buildAccountBalanceBody() (builder only) |
🔜 |
| B2C · Reversal · B2B | — | 🔜 |
The 🔜 initiator-credential APIs share an RSA SecurityCredential flow and an async
ResultURL receiver. Their typed request builders already ship (src/initiator.ts); the
live calls land in a future release. Follow along →
Daraja POSTs the real payment result to your callbackUrl as a deeply nested envelope.
parseStkCallback flattens it into one clean, typed object — and still works on failure
callbacks (which omit the metadata entirely):
import { parseStkCallback } from "@paylod/daraja";
app.post("/callback", (req, res) => {
const result = parseStkCallback(req.body);
if (result.success) {
// result.amount, result.mpesaReceiptNumber, result.phoneNumber, result.transactionDate
fulfilOrder(result.mpesaReceiptNumber, result.amount);
} else {
// Human-readable, categorized: result.decoded.title / .customerMessage / .retryable
console.warn(result.decoded.title); // e.g. "Payment cancelled by the customer"
}
res.json({ ResultCode: 0, ResultDesc: "Accepted" }); // always 200 Daraja
});Every failure is a DarajaError — inspect it, don't parse strings. Result codes are
decoded into human + customer-facing messages:
import { Daraja, DarajaError, decodeResultCode } from "@paylod/daraja";
try {
await daraja.stkPush({ /* ... */ });
} catch (err) {
if (DarajaError.is(err)) {
console.error(err.code, err.message, "retryable:", err.retryable);
if (err.decoded) console.error(err.decoded.customerMessage);
}
}
// Decode any result code directly:
decodeResultCode(2001).title; // "Wrong M-Pesa PIN"
decodeResultCode(2001).category; // "customer" (NOT a credentials problem!)
decodeResultCode(1032).customerMessage; // "Payment cancelled — you can try again…"- Create an app at developer.safaricom.co.ke and copy its Consumer Key + Consumer Secret.
- Use the shared sandbox shortcode
174379and its Lipa na M-Pesa passkey (both on the Daraja portal's "Lipa Na M-Pesa Online" test credentials page). - Expose a public HTTPS callback URL (e.g. an ngrok tunnel) — M-Pesa
can't reach
localhost. - Trigger a push, then use
simulateC2B()(sandbox only) to test C2B without a handset.
See examples/stk-push.ts for a runnable end-to-end script and
.env.example for the variables it needs.
// Handy: test numbers Safaricom accepts in sandbox
phoneNumber: "254708374149"new Daraja({
consumerKey: process.env.DARAJA_CONSUMER_KEY!, // required
consumerSecret: process.env.DARAJA_CONSUMER_SECRET!, // required
environment: "sandbox", // "sandbox" (default) | "production"
shortcode: process.env.DARAJA_SHORTCODE, // default shortcode (overridable per call)
passkey: process.env.DARAJA_PASSKEY, // required for STK
timeoutMs: 30000, // per-request timeout (default 30s)
fetch: customFetch, // inject your own fetch (proxy, retries, tests)
});Keep credentials in the environment, not in source. Copy .env.example
to .env, then load it however you like — Node 18+ has a built-in flag, no dependency needed:
node --env-file=.env your-script.jsBuilt on the global fetch and Web-standard APIs (TextEncoder, btoa,
AbortController) — no Buffer, no node:* imports. Runs unchanged on Node 18+,
Deno, Bun, Cloudflare Workers and other edge runtimes. Ships ESM + CJS + .d.ts.
We're paylod — a hosted M-Pesa "backend-as-a-service": no backend, no fees, no custody. We wrote this Daraja client for our own platform, tested it in production, and open-sourced the generic core because every Kenyan developer keeps rebuilding it.
This package is MIT licensed and deliberately independent of the paylod platform — no account, no lock-in. If you'd rather not host callbacks, reconcile missed webhooks, or manage credentials yourself, paylod.dev does that on top of exactly this code. Either way, take the SDK — it's yours. ⭐ the repo if it saved you a day.
Issues and PRs welcome — see CONTRIBUTING.md and our Code of Conduct.
MIT © 2026 paylod / Moses Mrima