ecitizen-pesaflow-gateway
v1.3.1
Published
Forward and backward compatible Node.js gateway and interactive setup CLI for Kenya eCitizen / PesaFlow payments
Maintainers
Readme
ecitizen-pesaflow-gateway
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 initto interactively configure your credentials, create.enventries, 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.envfrom your project directory. - Minimal-Dependency Core: The
EcitizenClient/EcitizenGatewaycore (signing, verification, HTTP submission) uses only Node.js's nativenode:cryptoandnode:http(s)— zero third-party dependencies when used as a library. The CLI itself depends ondotenvfor.envauto-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.
- CommonJS:
- 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-gatewayInteractive CLI Setup
Set up your project seamlessly directly from your terminal:
npx ecitizen-pesaflow initThe interactive wizard will:
- Prompt for your eCitizen credentials (API Client ID, API Key, Merchant Secret, Service ID).
- Ask for your target framework (Express, Fastify, Next.js App Router, Next.js Pages Router, NestJS, or Standalone).
- Automatically write or update your
.envfile. - Generate a typed configuration file (
ecitizen.config.jsorecitizen.config.ts). - 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 --yesCryptographic Verification Test
Verify your environment and HMAC algorithms against official test vectors:
npx ecitizen-pesaflow testPrompt & 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 0712345678Add --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-0001Open 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 0712345678It 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_URLrejects the poll requests (e.g.401/400 "Invalid token"), that's a limitation of the publicly-documented status API, not a bug incheckoutitself - 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=KES2. 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
- Keep Secrets Private: Store your API key and Merchant secret in
.env. Never commit secrets to git repositories. - CSRF Exemption: eCitizen servers POST webhook notifications from outside your application domain. If using CSRF protection (e.g.
csurfin Express), exempt the notification webhook route (/payment/notify). - Timing-Safe Comparison: Webhook signatures are checked using
node:crypto'stimingSafeEqualto prevent side-channel timing attacks. - Phone Formatting: Always use
PhoneHelper.normalize()to ensure phone numbers match Safaricom / Airtel STK push formats (2547XXXXXXXXor2541XXXXXXXX).
License
MIT License. See LICENSE for details.
