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

afrinex

v0.2.0

Published

Unified payments SDK for Kenya — Safaricom M-Pesa (Daraja) and KCB (Buni)

Readme

Afrinex SDK

A unified, dynamic payments SDK for Kenya — Standardizing M-Pesa (Daraja) and KCB (Buni) behind a single interface.

npm version License: MIT TypeScript

⭐️ Love Afrinex? Please consider giving it a star on GitHub! ⭐️ It helps the project grow, reach more developers in the ecosystem, and keeps the open-source maintenance going.


📖 The Idea Behind Afrinex

Integrating multiple payment gateways in Kenya is historically a fragmented, painful experience. Safaricom's Daraja API and KCB's Buni API have completely different shapes:

  • Authentication: Daraja uses Basic Auth to get a temporary OAuth token. Buni uses standard OAuth2.
  • Payloads: The JSON structure for an STK push is completely different.
  • Webhooks: Parsing a successful payment from Daraja is a nested nightmare compared to Buni's flatter structure.
  • Error Handling: Each returns errors in a proprietary format.

Afrinex was built to solve this fragmentation. The core philosophy is Unification through Abstraction. Afrinex provides a single, consistent interface that all payment gateways must implement. This means:

  • You learn one API (stkPush, payments.query, transfers.toPhone).
  • You handle one standard set of strongly-typed webhook payloads via handleWebhook.
  • You can dynamically load only the providers you need (reducing bundle size and mental overhead).

🏗️ Core Architecture

Afrinex is built around a flexible, plugin-like architecture:

  1. AfrinexClient: The central orchestrator created via createClient. It holds a registry of your instantiated providers, manages global configuration (like your callbackUrl and env), routes webhooks, and manages an internal event emitter.
  2. Providers (DarajaProvider, BuniProvider): Independent classes that implement the underlying provider interfaces. They handle the underlying HTTP requests, token caching, and payload normalization.
  3. Unified DTOs (Data Transfer Objects): Standardized request and response objects. When you call stkPush, you pass a generic request object. The provider translates this into the specific payload expected by Daraja or Buni.

🚀 Step-by-Step Integration Guide

1. Installation

npm install afrinex
# or
yarn add afrinex
# or
pnpm add afrinex

2. Procure Credentials

You need API credentials from the respective developer portals:

  • Daraja (Safaricom M-Pesa): Create an app at developer.safaricom.co.ke. You will need a Consumer Key, Consumer Secret, Shortcode, Passkey, Initiator Name, and Initiator Password.
  • Buni (KCB Bank): Create an app at buni.kcbgroup.com. You will need a Consumer Key, Consumer Secret, and Org Short Code.

3. Initialization

Instantiate the specific providers you need, then register them with the createClient factory.

import { createClient, DarajaProvider, BuniProvider } from 'afrinex';
import 'dotenv/config';

const env = 'sandbox'; // switch to 'production' when live

// 1. Initialize Providers
const daraja = new DarajaProvider({
  consumerKey: process.env.AFRINEX_DARAJA_CONSUMER_KEY!,
  consumerSecret: process.env.AFRINEX_DARAJA_CONSUMER_SECRET!,
  shortcode: process.env.AFRINEX_DARAJA_SHORTCODE!,
  passkey: process.env.AFRINEX_DARAJA_PASSKEY!,
  initiatorName: process.env.AFRINEX_DARAJA_INITIATOR_NAME!,
  initiatorPassword: process.env.AFRINEX_DARAJA_INITIATOR_PASSWORD!,
  // certPath: 'path/to/cert.cer' // Required for Daraja B2C/Balances in production
}, env);

const buni = new BuniProvider({
  consumerKey: process.env.AFRINEX_BUNI_CONSUMER_KEY!,
  consumerSecret: process.env.AFRINEX_BUNI_CONSUMER_SECRET!,
  orgShortCode: process.env.AFRINEX_BUNI_ORG_SHORT_CODE!
}, env);

// 2. Create the Client
const pay = createClient({
  env,
  callbackUrl: process.env.AFRINEX_CALLBACK_URL || 'https://api.yourdomain.com/webhooks',
  providers: {
    daraja,
    buni
  }
});

4. Executing Transactions

With the pay client initialized, you can retrieve a provider by name and execute standard commands seamlessly.

STK Push (C2B Express Checkout)

// For Daraja (M-Pesa)
const darajaRes = await pay.getProvider('daraja').stkPush({
  phone: '254708374149',
  amount: 1,
  reference: `INV-DARAJA-${Date.now()}`,
  description: 'Payment for services'
});
console.log('Daraja Checkout ID:', darajaRes.transactionId);

// For Buni (KCB)
const buniRes = await pay.getProvider('buni').stkPush({
  phone: '254722000000',
  amount: 1,
  reference: `INV-BUNI-${Date.now()}`,
  description: 'Payment for services'
});
console.log('Buni Checkout ID:', buniRes.transactionId);

B2C Transfers (Sending money to a phone)

const transferRes = await pay.getProvider('buni').transfers.toPhone({
  phone: '254722000000',
  amount: 100,
  reference: `B2C-${Date.now()}`,
  remarks: 'Salary Payment'
});

Querying Transaction Status

const queryRes = await pay.getProvider('buni').payments.query({
  transactionId: buniRes.transactionId
});
console.log('Payment Status:', queryRes.status);

5. Handling Webhooks & Event Emitters

Both Daraja and Buni send asynchronous HTTP POST callbacks to your callbackUrl. Afrinex simplifies this by exposing a single handleWebhook method on the client that parses the incoming payload into a standard UnifiedWebhookPayload.

// Example using Express.js
app.post('/webhooks/:provider', (req, res) => {
  const providerName = req.params.provider as 'daraja' | 'buni';
  
  try {
    // Automatically parse Daraja/Buni specific payloads into a unified format
    const event = pay.handleWebhook(providerName, req.body);
    
    if (event.event === 'payment.success') {
      console.log(`Received ${event.amount} from ${event.phone}`);
      console.log(`Transaction ID: ${event.transactionId}`);
      // Mark invoice as paid in your DB...
    } else {
      console.log(`Payment failed: ${event.transactionId}`);
    }

    // You can optionally emit internal events to resolve pending promises (like balances)
    pay.events.emit(`${event.event}:${providerName}`, event);
    
    res.sendStatus(200);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    res.sendStatus(400);
  }
});

🚧 Sandbox Limitations & Troubleshooting

When developing in the Daraja and Buni Sandbox environments, you will inevitably encounter intentional limitations that do not apply to production. Here are the most common sandbox errors and how to resolve them:

Buni: Account not whitelisted for FT API (Code 406 / 900908)

When calling transfers.toPhone() in the Buni Sandbox, you might receive an error stating Validation failed: Account not whitelisted for FT API or an Invalid Amount / Not Found error.

  • Why this happens: The Buni Sandbox strictly governs access to the Funds Transfer (FT/B2C) API. By default, newly created sandbox apps are not whitelisted to execute transfers.
  • The Fix: You must email KCB Support at [email protected]. Provide your Buni Developer Username and App Name, and explicitly request to be "whitelisted for the FT API in the sandbox environment."

Buni: Internal Server Error (Status 500) on Payments Query

When querying STK transactions using payments.query(), the Buni Sandbox Vending Gateway may successfully accept the payload format but ultimately crash downstream with a 500 Internal Server Error.

  • Why this happens: This is a known instability/limitation in the KCB Buni sandbox environment when checking the status of mock STK push transactions. Afrinex formats the payload correctly ({ payload: { requestId: ... } }), but the backend sandbox database occasionally fails to resolve it.

Daraja: Bad Request - Invalid Initiator (Code 400.002.02)

When calling balances() or B2C endpoints in the Daraja Sandbox, you might see this error.

  • Why this happens: You are either missing the Daraja security certificate (cert.cer), or the sandbox Initiator Name and Initiator Password provided do not match the sandbox defaults. Ensure you have properly generated the security credentials using the official Daraja sandbox public certificate.

Daraja: Spike Arrest / Too Many Requests (Code 500.001.1001 / 429)

When aggressively testing Daraja APIs (especially payments.query()), the sandbox API Gateway will block you with a Spike Arrest error.

  • Why this happens: The Daraja Sandbox limits the number of requests per second per IP/Account to simulate rate limiting.
  • The Fix: Implement delays/retries in your testing script, or simply wait a few seconds between requests.

📚 Error Handling

Afrinex exports custom Error classes for precise error handling:

  • AfrinexError: Base class for all SDK errors.
  • ConfigurationError: Thrown during initialization if credentials are missing or invalid.
  • AuthError: Thrown if token generation fails (e.g., invalid consumer key).
  • ProviderError: Thrown if the upstream provider rejects the request. It includes the original providerCode and providerMessage.
import { ProviderError } from 'afrinex';

try {
  await pay.getProvider('buni').transfers.toPhone({ ... });
} catch (error) {
  if (error instanceof ProviderError) {
    console.error(`Upstream rejected: ${error.providerMessage} (Code: ${error.providerCode})`);
    console.error('Raw response:', error.raw);
  }
}

🛠️ Guide for Contributors & Forking

Want to fork Afrinex to add a new provider?

Project Structure

packages/afrinex-sdk/
├── src/
│   ├── core/         # The AfrinexClient, EventEmitter, TokenManager, and HttpClient
│   ├── errors/       # Custom error classes
│   ├── providers/    # Individual provider implementations (daraja, buni)
│   ├── types/        # TypeScript interfaces and unified DTOs
│   └── index.ts      # Main export barrel
├── tests/            # Test suite

Development Scripts

  • yarn build: Compiles TS using tsup into ESM and CJS formats in the dist/ folder.
  • yarn test (in test-app): Runs the integration tests.

📜 License

MIT © Isaac Muigai