Skip to content

NowPesa/nodejs-sdk

Repository files navigation

@nowpesa/sdk — official NowPesa Node.js & TypeScript SDK

Take payments across M-Pesa, card, mobile money and bank transfer from a single integration. This package is the official, fully-typed Node.js client for the NowPesa REST API.

npm install @nowpesa/sdk
# or
pnpm add @nowpesa/sdk

Requires Node.js 18+ (uses the built-in fetch). ESM and CJS both ship in the same package; import and require both work.

Quick start

import { Nowpesa } from "@nowpesa/sdk";

const nowpesa = new Nowpesa({
  apiKey: {
    id: process.env.NOWPESA_KEY_ID!,
    secret: process.env.NOWPESA_KEY_SECRET!,
  },
});

const payment = await nowpesa.payments.create({
  reference: "order-1234",
  amount: 100_00,           // minor units — 100.00 KES
  currency: "KES",
  channel: "mpesa_stk_push",
  payer_phone: "+254712345678",
});

console.log(payment.id, payment.status);

The SDK returns plain JavaScript objects that mirror the REST shapes documented at https://nowpesa.com/docs/api.

Authentication

NowPesa uses HTTP Basic auth with an API key id + secret pair. Get keys from the dashboard, or bootstrap them programmatically:

const bootstrap = new Nowpesa({ apiKey: { id: "_", secret: "_" } });

const auth = await bootstrap.auth.register({
  email: "founder@acme.example",
  password: "...",
  merchant_name: "Acme",
  country: "KE",
});

const nowpesa = new Nowpesa({
  apiKey: { id: auth.api_key_id, secret: auth.api_key_secret },
});

Payments

// M-Pesa STK Push — async, completes via webhook + status update.
const stk = await nowpesa.payments.create({
  reference: "order-1",
  amount: 1500,
  currency: "KES",
  channel: "mpesa_stk_push",
  payer_phone: "+254712345678",
});

// Card — synchronous; resolves to `succeeded` (or `failed`) before
// returning.
const card = await nowpesa.payments.create({
  reference: "order-2",
  amount: 2500,
  currency: "USD",
  channel: "card",
  card_token: "tok_visa_test",
});

// Read one.
const fetched = await nowpesa.payments.get(stk.id);

// List with filters + cursor pagination.
const page = await nowpesa.payments.list({ status: "succeeded", limit: 50 });
if (page.next_before) {
  const next = await nowpesa.payments.list({
    status: "succeeded",
    limit: 50,
    before: page.next_before,
  });
}

// Refund (full or partial).
const refund = await nowpesa.payments.refund(card.id, { amount: 2500 });

Idempotency

Mutating requests (POST) auto-attach an Idempotency-Key (UUIDv4) so network retries don't double-charge. Set your own when you need explicit deduplication across processes:

await nowpesa.payments.create({
  reference: "order-3",
  amount: 1000,
  currency: "KES",
  channel: "mpesa_stk_push",
  payer_phone: "+254712345678",
  idempotencyKey: "order-3-v1",
});

Pass idempotencyKey: null to suppress (used for /v1/auth/*).

Payouts

const payout = await nowpesa.payouts.create({
  amount: 10_000_00,
  currency: "KES",
  destination_phone: "+254712345678",
  remarks: "weekly settlement",
});

Webhooks

Register an endpoint and verify signatures server-side:

const ep = await nowpesa.webhooks.register({
  url: "https://api.example.com/webhooks/nowpesa",
  event_types: ["payment.succeeded", "payment.refunded"],
});
console.log("Store this — shown once:", ep.signing_secret);

Verifying signatures

import { verifyWebhook, parseEvent, type WebhookEvent } from "@nowpesa/sdk";

app.post("/webhooks/nowpesa", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.header("X-Nowpesa-Signature");
  const ok = verifyWebhook(req.body, sig, process.env.NOWPESA_WEBHOOK_SECRET!);
  if (!ok) return res.status(400).end();

  const event = parseEvent<WebhookEvent>(req.body);
  switch (event.type) {
    case "payment.succeeded":
      // event.data is the payment payload
      break;
    case "payment.refunded":
      break;
  }
  res.status(200).end();
});

Secret rotation

The two-step rotate → promote flow lets you cut over to a new secret with zero dropped events. Until you call promoteSecret, deliveries carry both X-Nowpesa-Signature (current secret) and X-Nowpesa-Signature-Next (new secret). verifyWebhook accepts either:

const rotated = await nowpesa.webhooks.rotateSecret(ep.id);
// Store rotated.signing_secret_next.

// During cutover, verify either signature:
const ok = verifyWebhook(req.body, req.header("X-Nowpesa-Signature"), CURRENT, {
  nextSecret: NEW,
  nextHeader: req.header("X-Nowpesa-Signature-Next"),
});

// Once your fleet uses the new secret:
await nowpesa.webhooks.promoteSecret(ep.id);

Statement

const st = await nowpesa.statement.get({ since: "2026-01-01", currency: "KES" });
console.log(st.inbound_succeeded, st.payouts_succeeded);

// CSV download as a streamable Response:
const resp = await nowpesa.statement.downloadCsv({});
import { Writable } from "node:stream";
import { createWriteStream } from "node:fs";
await resp.body!.pipeTo(Writable.toWeb(createWriteStream("statement.csv")));

Error handling

Every method throws a typed NowpesaError subclass on non-2xx responses. Branch with instanceof:

import {
  NowpesaError,
  NowpesaValidationError,
  NowpesaRateLimitError,
  NowpesaServerError,
} from "@nowpesa/sdk";

try {
  await nowpesa.payments.create({ ... });
} catch (err) {
  if (err instanceof NowpesaValidationError) {
    // 400 — surface err.message to the user
  } else if (err instanceof NowpesaRateLimitError) {
    // 429 — err.retryAfter (seconds) honors the server's hint
  } else if (err instanceof NowpesaServerError) {
    // 5xx — already retried up to maxRetries times
  } else if (err instanceof NowpesaError) {
    console.error(err.status, err.type, err.message, err.requestId);
  }
}

Retry policy

The client retries on 429, 5xx, and network errors:

  • 3 retries by default (override with maxRetries).
  • 429 honors the Retry-After header.
  • 5xx + network: exponential backoff (1s, 2s, 4s) + 30% jitter.
  • 4xx (other than 429) are NOT retried — they're caller bugs.

Configuration

new Nowpesa({
  apiKey: { id, secret },
  baseUrl: "https://api.nowpesa.com",   // override for staging/dev
  timeoutMs: 30_000,                    // per-request timeout
  maxRetries: 3,
  defaultHeaders: { "X-My-App": "v1" }, // sent on every request
  fetch: customFetch,                   // inject for testing
});

Development

pnpm install
pnpm test       # unit tests + integration tests (auto-skipped if no edge-api)
pnpm typecheck
pnpm build

Integration tests hit a local edge-api at http://localhost:8080. They auto-skip if it's unreachable. Override:

NOWPESA_BASE_URL=http://localhost:8080 pnpm test
NOWPESA_INTEG=0 pnpm test          # force-skip integration suite
NOWPESA_SINK_HOST=127.0.0.1 pnpm test  # if edge-api runs on the host

License

MIT.

About

Official Node.js + TypeScript SDK for NowPesa — take M-Pesa, card, mobile money and bank payments with one API.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors