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

billpay

v0.2.0

Published

Framework-agnostic Node.js SDK for bill payment processing with automatic provider failover

Downloads

31

Readme

billpay

Framework-agnostic Node.js SDK for bill payment processing in Nigeria — with multi-provider support, unified abstractions, and full TypeScript support.

npm version License: MIT TypeScript Node.js


Overview

billpay gives you a single, consistent interface to process utility bill payments in Nigeria across multiple providers. Point it at InterSwitch, VTPass, or both — and it will handle the rest, including automatic failover if your primary provider goes down.

  • Pay airtime, data, TV subscriptions, electricity, and gaming bills through one unified API
  • Support for multiple providers — InterSwitch and VTPass behind a single interface
  • No database required — fully stateless; bring your own persistence
  • Drop into any framework — Express, Fastify, NestJS, plain Node.js, whatever you use

Table of Contents


Installation

npm install billpay
# or
yarn add billpay
# or
pnpm add billpay

Requirements: Node.js ≥ 16, npm ≥ 8


Quick Start

import { BillpayClient } from 'billpay';

const client = new BillpayClient({
  interswitch: {
    clientId: process.env.INTERSWITCH_CLIENT_ID!,
    secretKey: process.env.INTERSWITCH_SECRET_KEY!,
    terminalId: process.env.INTERSWITCH_TERMINAL_ID!,
    apiBaseUrl: 'https://sandbox.quickteller.com',
    authUrl: 'https://sandbox.quickteller.com/api/v5/Auth/GetAccessToken',
    paymentReferencePrefix: 'BPY_',
  },
  vtpass: {
    apiKey: process.env.VTPASS_API_KEY!,
    secretKey: process.env.VTPASS_SECRET_KEY!,
    publicKey: process.env.VTPASS_PUBLIC_KEY!,
    apiBaseUrl: 'https://sandbox.vtpass.com/api',
    phone: '08011111111',
  },
});

// Set active provider
client.setProviderPreference('INTERSWITCH');

// Browse available plans
const plans = await client.getPlans({ provider: 'BOTH' });

// Find the plan you need
const mtnPlan = plans.find(p => p.billerName === 'MTN' && p.amount === 50000);

// Pay
const result = await client.pay({
  billingItemId: mtnPlan.id,
  paymentReference: 'unique-ref-001',
  billerItem: mtnPlan,
  customerId: '08012345678',
  amount: 50000,
});

console.log(result);

Initialization

Multi-provider

Pass both interswitch and vtpass configurations to BillpayClient to use both providers:

import { BillpayClient } from 'billpay';

const client = new BillpayClient({
  interswitch: { /* InterSwitch config */ },
  vtpass:      { /* VTPass config */      },
});

client.setProviderPreference('INTERSWITCH');

When pay() is called, the SDK uses whichever provider is set as primary. Each provider is tried once; there is no automatic fallback between providers.

Single-provider clients

If you only integrate one provider, import the dedicated client instead:

import { InterswitchClient from 'billpay/interswitch' };
import { VtpassClient }      from 'billpay/vtpass';

// InterSwitch only
const isClient = new InterswitchClient({
  interswitch: { /* config */ },
});

// VTPass only
const vtClient = new VtpassClient({
  vtpass: { /* config */ },
});

Single-provider clients expose the same full interface (getPlans, pay, validateCustomer, confirmTransaction, getCategories)

You can also achieve the same result with BillpayClient by supplying only one provider:

// Equivalent to InterswitchClient
const client = new BillpayClient({ interswitch: { /* config */ } });

Core Concepts

Provider preference

client.setProviderPreference('INTERSWITCH');

This sets InterSwitch as the active provider for all subsequent pay(), getPlans(), validateCustomer(), and getCategories() calls. To switch to VTPass:

client.setProviderPreference('VTPASS');

To use a specific provider for a single call without changing the global preference:

const result = await client.pay({
  ...paymentRequest,
  provider: 'VTPASS',
});

Check the current preference at any time:

const { primary, fallback } = client.getActiveProviders();

Stateless architecture

The SDK holds no persistent state and has no database dependency. Every call is self-contained. This means:

  • You own all transaction records (pending, failed, successful).

API Reference

BillpayClient

The main entry point. Accepts one or both provider configurations.

new BillpayClient(config: BillpayClientConfig)

setProviderPreference(primary, fallback?)

client.setProviderPreference('INTERSWITCH');
client.setProviderPreference('VTPASS');

getActiveProviders()

Returns { primary: ProviderType, fallback: ProviderType | null }.

getCategories(provider?)

const categories = await client.getCategories('BOTH');
// => BillpayCategory[]
// e.g. [{ id: '1', name: 'Airtime' }, { id: '2', name: 'Data' }, ...]

Fetches unified bill categories (Airtime, Data, TV, Electricity, Gaming). When 'BOTH' is specified, duplicates are removed and results are merged.

| Parameter | Type | Default | Description | |-----------|------|---------|-------------| | provider | 'INTERSWITCH' \| 'VTPASS' \| 'BOTH' | current primary | Which provider to fetch categories from |

getPlans(options?)

const plans = await client.getPlans({
  provider: 'BOTH',
  filters: {
          vtpass: {
            "ELECTRICITY-BILL": ["Yola Electric Disco Payment - YEDC"],
          },
          interswitch: {
            "Cable TV Bills": ["DAARSAT Communications"],
          },
    },
});
// => BillerItem[]
//  [{
  //   category: 'Cable TV Bills',
  //   billerName: 'DAARSAT Communications',
  //   name: 'Single Package',
  //   amount: 600000,
  //   amountType: 5,
  //   active: true,
  //   paymentCode: '11310',
  //   billerId: '113',
  //   provider: 'INTERSWITCH'
  // },
  // ...
  //]

Fetches available billing plans. Provider-specific filters are passed directly to the underlying provider API.

| Option | Type | Description | |--------|------|-------------| | provider | ProviderType \| 'BOTH' | Which provider to query | | filters.interswitch | object | Raw InterSwitch filter params | | filters.vtpass | object | Raw VTPass filter params |

validateCustomer(request)

Validates a customer identifier before processing payment. Use this for electricity meters, decoder smartcard numbers, etc.

const customer = await client.validateCustomer({
  customerId: '45300023208',   // meter number, smartcard, phone, etc.
  paymentCode: plan.paymentCode,
  provider: 'INTERSWITCH',     // optional; defaults to primary
});
// => Customer { name, address, ... }

pay(request)

Executes a bill payment using the configured provider.

const result = await client.pay({
  billingItemId:    mtnPlan.id,
  paymentReference: 'unique-ref-001',  // must be globally unique per transaction
  billerItem:       mtnPlan,
  customerId:       '08012345678',
  amount:           50000,             // in kobo (50,000 kobo = ₦500)
  provider:         'INTERSWITCH',     // optional; overrides preference & disables failover
});
// => PayResponse

Important: paymentReference must be unique per transaction. Reusing a reference may cause your provider to reject or misroute the payment.

confirmTransaction(reference, provider?)

Requery the status of a previously executed transaction.

const status = await client.confirmTransaction('unique-ref-001');
// => PayResponse { status, ... }

If provider is omitted, the SDK queries the primary provider. Pass a specific provider if you know which one processed the original payment.


InterswitchClient

import InterswitchClient from 'billpay/interswitch';

new InterswitchClient({ interswitch: InterswitchConfig })

Exposes the same interface as BillpayClient. The provider parameter on any method is ignored (always uses InterSwitch). Attempting to set a VTPass fallback has no effect.


VtpassClient

import VtpassClient from 'billpay/vtpass';

new VtpassClient({ vtpass: VtpassConfig })

Same interface as BillpayClient. Always uses VTPass; provider overrides are ignored.


Common Workflows

Airtime top-up

// 1. Get all airtime plans
const plans = await client.getPlans({
  provider: 'BOTH',
  filters: { vtpass: { serviceID: ['mtn'] } },
});

// 2. Pick a plan
const plan = plans.find(p => p.billerName === 'MTN' && p.amount === 10000);

// 3. Pay (no customer validation required for airtime)
const result = await client.pay({
  billingItemId: plan.id,
  paymentReference: `AIRTIME-${Date.now()}`,
  billerItem: plan,
  customerId: '08012345678', // recipient phone number
  amount: plan.amount,
});

Electricity payment

// 1. Get electricity plans
const plans = await client.getPlans({
  provider: 'INTERSWITCH',
  filters: { interswitch: { categoryId: ['4'] } }, // electricity category
});

const plan = plans.find(p => p.billerName.includes('EKEDC'));

// 2. Validate the meter number first
const customer = await client.validateCustomer({
  customerId: '45300023208',
  paymentCode: plan.paymentCode,
});
console.log(`Validated: ${customer.name} at ${customer.address}`);

// 3. Pay
const result = await client.pay({
  billingItemId: plan.id,
  paymentReference: `ELEC-${Date.now()}`,
  billerItem: plan,
  customerId: '45300023208',
  amount: 500000, // ₦5,000 in kobo
});

TV subscription

const plans = await client.getPlans({
  provider: 'BOTH',
  filters: { vtpass: { serviceID: ['dstv'] } },
});

const plan = plans.find(p => p.name.includes('Compact'));

// Validate smartcard
const customer = await client.validateCustomer({
  customerId: '7042552048',
  paymentCode: plan.paymentCode,
});

const result = await client.pay({
  billingItemId: plan.id,
  paymentReference: `TV-${Date.now()}`,
  billerItem: plan,
  customerId: '7042552048',
  amount: plan.amount,
});

Transaction confirmation

Always confirm after payment — especially in webhook-driven or async flows:

const status = await client.confirmTransaction('unique-ref-001');

if (status.responseCode === '00') {
  // Success — update your records
} else {
  // Handle failure or pending state
}

Configuration Reference

InterSwitchConfig

| Field | Type | Required | Description | |-------|------|----------|-------------| | clientId | string | ✅ | Your InterSwitch client ID | | secretKey | string | ✅ | Your InterSwitch secret key | | terminalId | string | ✅ | Your terminal ID | | apiBaseUrl | string | ✅ | API base URL (sandbox or production) | | authUrl | string | ✅ | OAuth token URL | | paymentReferencePrefix | string | ❌ | Prefix for auto-generated payment references |

VtpassConfig

| Field | Type | Required | Description | |-------|------|----------|-------------| | apiKey | string | ✅ | Your VTPass API key | | secretKey | string | ✅ | Your VTPass secret key | | publicKey | string | ✅ | Your VTPass public key | | apiBaseUrl | string | ✅ | API base URL (sandbox or production) | | phone | string | ✅ | Phone number associated with the account |

Sandbox vs Production URLs

| Provider | Sandbox | Production | |----------|---------|------------| | InterSwitch | https://sandbox.quickteller.com | https://api.quickteller.com | | VTPass | https://sandbox.vtpass.com/api | https://vtpass.com/api |


Error Handling

The SDK throws standard JavaScript Error objects with descriptive messages.

try {
  const result = await client.pay(paymentRequest);
} catch (error) {
  if (error instanceof Error) {
    console.error('Payment failed:', error.message);
    // error.message describes which providers were attempted and why each failed
  }
}

Common error scenarios and what they mean:

| Scenario | Behaviour | |----------|-----------| | Provider call fails | Error thrown with the provider's error message | | provider override targets unconfigured provider | Error thrown immediately before any network call | | No providers configured | Error thrown at construction time | | Invalid paymentReference reuse | Provider-level error surfaced as thrown Error |


Environment Variables

Store credentials in environment variables and never commit them to source control.

# InterSwitch
INTERSWITCH_CLIENT_ID=your_client_id
INTERSWITCH_SECRET_KEY=your_secret_key
INTERSWITCH_TERMINAL_ID=your_terminal_id
INTERSWITCH_MERCHANT_CODE=your_merchant_code

# VTPass
VTPASS_API_KEY=your_api_key
VTPASS_SECRET_KEY=your_secret_key
VTPASS_PUBLIC_KEY=your_public_key

Use dotenv (included as a dependency) or your runtime's native secret management:

import 'dotenv/config';
import { BillpayClient } from 'billpay';

const client = new BillpayClient({
  interswitch: {
    clientId:  process.env.INTERSWITCH_CLIENT_ID!,
    secretKey: process.env.INTERSWITCH_SECRET_KEY!,
    terminalId: process.env.INTERSWITCH_TERMINAL_ID!,
    apiBaseUrl: 'https://sandbox.quickteller.com',
    authUrl:    'https://sandbox.quickteller.com/api/v5/Auth/GetAccessToken',
  },
});

Contributing

Contributions are welcome! To get started:

git clone https://github.com/deveasyclick/billpay-sdk.git
cd billpay-sdk
pnpm install

Useful scripts:

pnpm build        # Compile TypeScript
pnpm test         # Run tests (vitest)
pnpm test:watch   # Watch mode
pnpm lint         # ESLint
pnpm type-check   # tsc without emit

Please open an issue before submitting a PR for significant changes. Bug fixes and documentation improvements are always welcome without prior discussion.


Changelog

See CHANGELOG.md for a full version history.

Latest: [0.1.0] — 2026-03-06 — Initial release with InterSwitch and VTPass support, unified category abstractions, and full TypeScript types.


License

MIT © deveasyclick