DEV Community

Cover image for A unified TypeScript SDK for Nigerian fintech providers...
Oluwafemi Awoyemi
Oluwafemi Awoyemi

Posted on

A unified TypeScript SDK for Nigerian fintech providers...

Stop Rewriting Your Paystack Integration — I Built a Unified Nigerian Fintech SDK

If you've built more than one Nigerian fintech product, you've written the same code at least twice.

Paystack integration. Then a client wants Flutterwave. Then another wants Monnify because their bank has a deal with them. Each time, you're not just swapping an API key — you're rewriting your payment initialization, your webhook handler, your transfer logic, your error handling. The shape of every response is different. The status strings are different. Even the amount units are different (Paystack uses kobo, Flutterwave uses naira, Monnify uses naira).

I got tired of it. So I built ng-pay.


What It Is

ng-pay is a TypeScript SDK that puts Paystack, Flutterwave, and Monnify behind one interface. You write your payment code once. Switching providers — or supporting multiple at once — is a one-line change.

// Change this one line to switch providers
const provider = new PaystackProvider({ secretKey: process.env.PAYSTACK_SECRET_KEY! });
// const provider = new FlutterwaveProvider({ secretKey: process.env.FLW_SECRET_KEY! });
// const provider = new MonnifyProvider({ apiKey: '...', secretKey: '...', contractCode: '...' });

// Everything else stays the same
const payment = await provider.initializePayment({ ... });
const banks   = await provider.getBanks();
const account = await provider.resolveAccount('0123456789', '058');
Enter fullscreen mode Exit fullscreen mode

Every adapter implements the same NgPayProvider interface, so your application code never knows or cares which provider is underneath.


The Problems It Solves

1. Amount unit confusion

Paystack expects kobo. Flutterwave expects naira. Monnify expects naira. ng-pay normalizes everything — you always work in kobo internally, and the adapters handle conversion at the boundary.

import { toKobo } from '@ng-pay/core';

// This works identically for all three providers
await provider.initializePayment({
  amount: { amount: toKobo(5000), currency: 'NGN' }, // ₦5,000
  customer: { email: 'customer@example.com' },
});
Enter fullscreen mode Exit fullscreen mode

2. Status string normalization

Paystack returns "success". Flutterwave returns "successful". Monnify returns "PAID" or "OVERPAID". ng-pay maps all of them to a consistent PaymentStatus type:

result.status // always: 'success' | 'failed' | 'pending' | 'abandoned' | 'processing'
Enter fullscreen mode Exit fullscreen mode

3. Webhook boilerplate

Every provider has a different signature header, a different signing algorithm, and a different event shape. ng-pay handles all of it:

// Paystack:    x-paystack-signature header, HMAC-SHA512
// Flutterwave: verif-hash header, plain comparison
// Monnify:     monnify-signature header, HMAC-SHA512

// With ng-pay — same code for all three:
if (!provider.verifyWebhook(rawBody, signature)) {
  return res.status(401).send('Invalid signature');
}

const event = provider.parseWebhookEvent(JSON.parse(rawBody));

if (event.event === 'charge.success') {
  await fulfillOrder(event.reference!);
}
Enter fullscreen mode Exit fullscreen mode

Or use @ng-pay/middleware and skip even that:

app.post(
  '/webhooks/paystack',
  express.raw({ type: 'application/json' }),
  ngPayWebhook({
    provider: paystack,
    onEvent: async (event) => {
      if (event.event === 'charge.success') {
        await fulfillOrder(event.reference!);
      }
    },
  })
);
Enter fullscreen mode Exit fullscreen mode

4. Error handling

Every provider throws different error shapes. ng-pay normalizes them all into typed errors you can actually handle:

import { isNgPayError, isRateLimitError } from '@ng-pay/core';

try {
  await provider.initiateTransfer({ ... });
} catch (err) {
  if (isRateLimitError(err)) {
    await sleep(err.retryAfter! * 1000);
  } else if (isNgPayError(err)) {
    console.error(err.code);    // 'INSUFFICIENT_BALANCE' | 'INVALID_PARAMS' | ...
    console.error(err.provider); // 'paystack' | 'flutterwave' | 'monnify'
  }
}
Enter fullscreen mode Exit fullscreen mode

Packages

ng-pay is a monorepo with five packages:

Package What it does
@ng-pay/core Shared types, HTTP client, error classes
@ng-pay/paystack Paystack adapter
@ng-pay/flutterwave Flutterwave adapter
@ng-pay/monnify Monnify adapter
@ng-pay/middleware Express, NestJS, Fastify webhook helpers

Quick Example — Full Payment Flow

import { PaystackProvider } from '@ng-pay/paystack';
import { toKobo } from '@ng-pay/core';

const paystack = new PaystackProvider({
  secretKey: process.env.PAYSTACK_SECRET_KEY!,
});

// 1. Initialize
const payment = await paystack.initializePayment({
  amount: { amount: toKobo(5000), currency: 'NGN' },
  customer: { email: 'customer@example.com', name: 'Adaobi Nwosu' },
  callbackUrl: 'https://yourapp.com/callback',
});

// Redirect flow
redirect(payment.authorizationUrl);

// Or inline checkout
PaystackPop.setup({ key: 'pk_live_...', accessCode: payment.accessCode });

// 2. Verify
const result = await paystack.verifyPayment(payment.reference);
console.log(result.status);  // 'success'
console.log(result.amount);  // { amount: 500000, currency: 'NGN' }

// 3. Virtual account
const account = await paystack.createVirtualAccount({
  customer: { email: 'customer@example.com', name: 'Adaobi Nwosu' },
  splitCode: 'SPL_ab3defgh', // optional — split revenue
});

// 4. Payout
const recipient = await paystack.createTransferRecipient({
  name: 'Adaobi Nwosu',
  accountNumber: '0123456789',
  bankCode: '058',
});

await paystack.initiateTransfer({
  amount: { amount: toKobo(1000), currency: 'NGN' },
  recipientCode: recipient.recipientCode,
  description: 'Refund',
});
Enter fullscreen mode Exit fullscreen mode

What's Built In

Things I didn't want to think about every project, now handled automatically:

Retry with exponential backoff — 5xx errors and timeouts are retried automatically with jitter. Rate limit responses respect the retry-after header.

Monnify OAuth — Monnify requires a token exchange before every session. ng-pay handles it transparently — you never call /auth/login yourself.

Secure credential storage — API keys are stored as non-enumerable properties. They won't appear in JSON.stringify, console.log, Object.keys, or Sentry error reports.

Timing-safe webhook verification — All providers use constant-time HMAC comparison to prevent timing oracle attacks.

Monnify environment inference — Pass MK_TEST_xxx and it uses sandbox. Pass MK_LIVE_xxx and it uses production. Ambiguous keys throw rather than silently hitting the wrong environment.


Installing

npm install @ng-pay/core @ng-pay/paystack
# or
npm install @ng-pay/core @ng-pay/flutterwave
# or
npm install @ng-pay/core @ng-pay/monnify
Enter fullscreen mode Exit fullscreen mode

For webhook middleware:

npm install @ng-pay/middleware
Enter fullscreen mode Exit fullscreen mode

What's Next

The roadmap includes Ghana (Hubtel), Kenya (M-Pesa), a multi-provider failover router, and Python bindings. Contributions are open — if you want to add a provider, there's a full guide in CONTRIBUTING.md.

GitHub: github.com/ProfoundLabs/ng-pay

npm: @ng-pay/core · @ng-pay/paystack · @ng-pay/flutterwave · @ng-pay/monnify


If you're building Nigerian fintech and you find this useful — or if you find something broken — open an issue or drop a comment. Would love to hear how people are using it.

Top comments (0)