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

ecitizen-pesaflow-gateway

v1.3.1

Published

Forward and backward compatible Node.js gateway and interactive setup CLI for Kenya eCitizen / PesaFlow payments

Readme

ecitizen-pesaflow-gateway

npm version CI License: MIT Node.js Version TypeScript

A beginner-friendly Kenya eCitizen / PesaFlow payment gateway extension and SDK for Node.js, Express, Fastify, Next.js, NestJS, and Vanilla JavaScript / TypeScript. Build signed checkout payloads, render instant payment buttons, and verify webhook callbacks with plain-English fields.

  • Forward & Backward Compatible: Dual-distributed in CommonJS (require) and ES Modules (import) with full TypeScript declarations (.d.ts). Fully compatible across Node.js 16, 18, 20, 22, and 24+.
  • Interactive CLI Setup: Run npx ecitizen-pesaflow init to interactively configure your credentials, create .env entries, and scaffold framework-specific payment routes in seconds.
  • Headless Payment CLI: Prompt and check on payments straight from the terminal with npx ecitizen-pesaflow pay / status — no browser, no HTML, no scaffolding required. The CLI auto-loads .env from your project directory.
  • Minimal-Dependency Core: The EcitizenClient/EcitizenGateway core (signing, verification, HTTP submission) uses only Node.js's native node:crypto and node:http(s) — zero third-party dependencies when used as a library. The CLI itself depends on dotenv for .env auto-loading convenience.
  • Instant Payment Button: Render ready-to-use, HMAC-signed payment forms in one line of code (payButton()) or retrieve raw payloads (checkout()) for custom React/Vue/mobile UIs.
  • Safaricom M-Pesa STK Push: Built-in Kenyan phone normalization (PhoneHelper) to trigger instant PIN prompts on customer phones (07..., 01..., +254... -> 2547...).
  • Timing-Safe Cryptographic Verification: Validate server-to-server IPN notifications with timing-safe HMAC-SHA256 (verify(), isPaid()).
  • Pre-Built Adapters: Out-of-the-box webhook handlers and middlewares for Express, Fastify, Next.js (App Router & Pages Router), and native HTTP.

Compatibility

  • Node.js: >= 16.0.0 (fully tested on Node 16, 18, 20, 22, and 24+).
  • Module Systems:
    • CommonJS: const { EcitizenClient } = require('ecitizen-pesaflow-gateway');
    • ES Modules: import { EcitizenClient } from 'ecitizen-pesaflow-gateway';
    • TypeScript: Full IntelliSense and type checking included.
  • Web Frameworks: Express, Fastify, Next.js (App & Pages Router), NestJS, Koa, Hono, and Vanilla Node.js http.

Installation

npm install ecitizen-pesaflow-gateway
# or
yarn add ecitizen-pesaflow-gateway
# or
pnpm add ecitizen-pesaflow-gateway

Interactive CLI Setup

Set up your project seamlessly directly from your terminal:

npx ecitizen-pesaflow init

The interactive wizard will:

  1. Prompt for your eCitizen credentials (API Client ID, API Key, Merchant Secret, Service ID).
  2. Ask for your target framework (Express, Fastify, Next.js App Router, Next.js Pages Router, NestJS, or Standalone).
  3. Automatically write or update your .env file.
  4. Generate a typed configuration file (ecitizen.config.js or ecitizen.config.ts).
  5. Scaffold a complete, ready-to-run Payment & Webhook controller for your selected framework!

Non-Interactive / CI Flags

For CI/CD pipelines or headless scripts:

npx ecitizen-pesaflow init --client-id "MY_ID" --api-key "MY_KEY" --secret "MY_SEC" --service-id "MY_SVC" --framework express --yes

Cryptographic Verification Test

Verify your environment and HMAC algorithms against official test vectors:

npx ecitizen-pesaflow test

Prompt & Check Payments (Headless, No UI)

Sign and submit a payment directly to eCitizen from the terminal — no browser, no scaffolding. Credentials are read from ECITIZEN_* env vars / .env (auto-loaded):

npx ecitizen-pesaflow pay --amount 500 --reference INV-0001 --description "School fees" \
  --name "Jane Doe" --id-number 12345678 --phone 0712345678

Add --dry-run to build and print the signed payload without sending it, useful for inspecting the exact request/hash before going live. By default you'll be asked to confirm before anything is actually submitted; pass --yes/-y to skip that in scripts/CI.

Check settlement status for a previously submitted reference (requires ECITIZEN_STATUS_URL / --status-url):

npx ecitizen-pesaflow status --reference INV-0001

Open the Real Payment Page in a Browser, and Watch for Settlement

If you'd rather the payer complete checkout in an actual browser (M-Pesa Paybill/STK options, card, bank, etc. - eCitizen's own page) instead of a raw server-to-server POST, checkout launches your default browser straight to it and keeps polling in the background so the CLI process detects settlement even after you close the window:

npx ecitizen-pesaflow checkout --amount 500 --reference INV-0001 --description "School fees" \
  --name "Jane Doe" --id-number 12345678 --phone 0712345678

It starts a tiny local server serving a self-submitting form (so the browser tab lands directly on eCitizen's page, not an intermediate blank one), opens it in your OS's default browser, then polls ECITIZEN_STATUS_URL every --poll-interval seconds (default 5s) until it sees a success status, --timeout elapses (default 600s), or you press Ctrl+C. Pass --no-open to just print the URL instead of auto-launching a browser.

Known limitation: eCitizen's own payment page uses a more specific, apparently browser-session-authenticated status endpoint internally. A headless CLI process doesn't have that session, so if your ECITIZEN_STATUS_URL rejects the poll requests (e.g. 401/400 "Invalid token"), that's a limitation of the publicly-documented status API, not a bug in checkout itself - the browser-based payment flow still works regardless.

Run npx ecitizen-pesaflow help for the full flag reference.


Quickstart: Express.js

1. Configure Environment Variables (.env)

ECITIZEN_CLIENT_ID=your_api_client_id
ECITIZEN_API_KEY=your_api_key
ECITIZEN_SECRET=your_merchant_secret
ECITIZEN_SERVICE_ID=your_service_id
ECITIZEN_GATEWAY_URL=https://payments.ecitizen.go.ke/PaymentAPI/iframev2.1.php
ECITIZEN_CURRENCY=KES

2. Create Payment Controller (controllers/paymentController.js)

const express = require('express');
const { EcitizenClient, PhoneHelper, createExpressWebhookHandler } = require('ecitizen-pesaflow-gateway');

// Automatically reads from process.env if no parameters are passed
const client = new EcitizenClient();
const paymentRouter = express.Router();

/**
 * 1. Checkout & Pay Button View
 */
paymentRouter.get('/pay', (req, res) => {
  const amount = Number(req.query.amount || 1500);
  const reference = String(req.query.reference || ('INV-' + Date.now()));

  const payButtonHtml = client.payButton({
    amount: amount,
    reference: reference,
    description: 'Land Rates Clearance',
    name: 'John Doe',
    idNumber: '28374619',
    phone: PhoneHelper.normalize('0712345678'), // Triggers Safaricom M-Pesa STK push
    sendStkPush: true,
    callbackUrl: `${req.protocol}://${req.get('host')}/payment/success?reference=${reference}`,
    notifyUrl: `${req.protocol}://${req.get('host')}/payment/notify`,
  }, 'Proceed to eCitizen', { class: 'btn btn-success btn-lg' });

  res.send(`
    <div style="max-width: 480px; margin: 50px auto; text-align: center; font-family: sans-serif;">
      <h2>Invoice #${reference}</h2>
      <p>Amount: <strong>KES ${amount.toFixed(2)}</strong></p>
      <div style="margin-top: 20px;">
        ${payButtonHtml}
      </div>
    </div>
  `);
});

/**
 * 2. Server-to-Server IPN Notification Webhook
 * Cryptographically verifies HMAC signature and confirms settlement.
 */
paymentRouter.post('/notify', createExpressWebhookHandler(client, {
  onSuccess: async (result, req, res) => {
    console.log(`Payment confirmed for reference: ${result.reference}, amount: ${result.amountPaid}`);
    // Example: update database record
    // await Order.updateOne({ reference: result.reference }, { status: 'paid' });
  },
  onFailure: async (result, req, res) => {
    console.warn(`Payment verification failed: ${result.description}`);
  }
}));

/**
 * 3. Browser Return Landing Page
 */
paymentRouter.get('/success', (req, res) => {
  res.send(`<h3>Payment Submitted! Reference: ${req.query.reference}</h3>`);
});

module.exports = paymentRouter;

3. Mount in your Express App (server.js)

Make sure URL-encoded body parser is enabled for webhooks:

const express = require('express');
const paymentRouter = require('./controllers/paymentController');

const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.use('/payment', paymentRouter);

app.listen(3000, () => console.log('Server running on port 3000'));

Quickstart: Next.js (App Router)

1. Webhook Route Handler (app/api/ecitizen/notify/route.ts)

import { EcitizenClient, createNextAppRouteHandler } from 'ecitizen-pesaflow-gateway';

const client = new EcitizenClient();

export const POST = createNextAppRouteHandler(client, {
  onSuccess: async (result, rawBody) => {
    console.log('[eCitizen] Confirmed payment:', result.reference, result.amountPaid);
    // await prisma.order.update({ where: { ref: result.reference }, data: { paid: true } });
  },
  onFailure: async (result) => {
    console.warn('[eCitizen] Verification failed:', result.reference);
  }
});

2. Initiate Payment from Next.js Server Component or Route

import { EcitizenClient, PhoneHelper } from 'ecitizen-pesaflow-gateway';

const client = new EcitizenClient();

export async function createCheckout(order: { id: string; amount: number; user: any }) {
  const checkout = client.checkout({
    amount: order.amount,
    reference: order.id,
    description: 'Order Payment',
    name: order.user.name,
    idNumber: order.user.nationalId,
    phone: PhoneHelper.normalize(order.user.phone),
    sendStkPush: true,
    callbackUrl: 'https://yourdomain.com/payment/success',
    notifyUrl: 'https://yourdomain.com/api/ecitizen/notify',
  });

  return checkout; // { url: 'https://payments.ecitizen...', payload: { ... } }
}

Standalone Usage (Pure JavaScript / TypeScript)

import { EcitizenClient, PhoneHelper } from 'ecitizen-pesaflow-gateway';

const ecitizen = new EcitizenClient({
  apiClientID: 'YOUR_API_CLIENT_ID',
  apiKey: 'YOUR_API_KEY',
  secret: 'YOUR_SECRET',
  serviceID: 'YOUR_SERVICE_ID',
});

// 1. Generate a signed checkout payload
const { url, payload } = ecitizen.checkout({
  amount: 500,
  reference: 'INV-0001',
  description: 'School fees',
  name: 'Jane Doe',
  idNumber: '12345678',
  phone: '0712345678', // Automatically normalized to 254712345678
});

// 2. Verify an inbound webhook notification
const result = ecitizen.verify(webhookPayload);
if (result.success) {
  console.log(`Payment confirmed for ${result.reference} (${result.amountPaid} KES)`);
} else {
  console.error(`Verification failed: ${result.description}`);
}

// 3. Or skip the browser/HTML entirely and prompt the payment directly
// from your server (e.g. triggers an M-Pesa STK push):
const submission = await ecitizen.initiatePayment({
  amount: 500,
  reference: 'INV-0002',
  description: 'School fees',
  name: 'Jane Doe',
  idNumber: '12345678',
  phone: '0712345678',
  sendStkPush: true,
});
console.log(submission.httpStatus, submission.responseBody);

// 4. Poll settlement status (requires `statusUrl` / ECITIZEN_STATUS_URL)
const status = await ecitizen.checkPaymentStatus('INV-0002');
console.log(status.httpStatus, status.responseBody);

API Reference Overview

| Method / Utility | Description | |---|---| | client.checkout(...) | Generates signed checkout payload and endpoint URL | | client.payButton(...) | Generates self-contained HTML form and payment button | | client.initiatePayment(...) | Directly submits payment without browser/HTML | | client.checkPaymentStatus(...) | Polls payment settlement status | | client.verify(payload) | Verifies IPN webhook HMAC signature | | client.isPaid(payload) | Returns boolean verification result | | PhoneHelper.normalize(phone) | Normalizes Kenyan phone numbers (07..., 01... -> 254...) | | npx ecitizen-pesaflow init | Interactive project setup wizard |


Security & Best Practices

  1. Keep Secrets Private: Store your API key and Merchant secret in .env. Never commit secrets to git repositories.
  2. CSRF Exemption: eCitizen servers POST webhook notifications from outside your application domain. If using CSRF protection (e.g. csurf in Express), exempt the notification webhook route (/payment/notify).
  3. Timing-Safe Comparison: Webhook signatures are checked using node:crypto's timingSafeEqual to prevent side-channel timing attacks.
  4. Phone Formatting: Always use PhoneHelper.normalize() to ensure phone numbers match Safaricom / Airtel STK push formats (2547XXXXXXXX or 2541XXXXXXXX).

License

MIT License. See LICENSE for details.