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

@sconyema/paystack-js

v0.1.0

Published

A fully-typed TypeScript SDK for the Paystack API

Readme

paystack-js

A fully-typed TypeScript SDK for the Paystack API

npm version License: MIT Tests


Features

  • Full TypeScript types for every request param, response object, and error class
  • Dual CJS + ESM output — works with require or import, ships .d.ts declarations for both
  • 10 resource areas — Transactions, Customers, Plans, Subscriptions, Transfers, Recipients, Subaccounts, Splits, Refunds, Webhooks
  • Automatic retries with exponential backoff and jitter on 429 and 5xx responses
  • Response envelope unwrapping — methods return data directly; no response.data.data boilerplate
  • Typed error classes — catch PaystackAuthError, PaystackNotFoundError, PaystackValidationError, etc. by type
  • Webhook signature verification using HMAC-SHA512 and timingSafeEqual

Installation

npm install paystack-js
pnpm add paystack-js
yarn add paystack-js

Requires Node.js ≥ 18.


Quick Start

import Paystack from 'paystack-js';

const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!);

// Amounts are in the smallest currency unit — kobo for NGN (₦500 = 50000)
const { authorization_url, reference } = await paystack.transactions.initialize({
  email: '[email protected]',
  amount: 50000,
});

// Redirect the customer to authorization_url, then verify on callback
const transaction = await paystack.transactions.verify(reference);

if (transaction.status === 'success') {
  console.log(`Payment confirmed: ₦${transaction.amount / 100}`);
}

Usage

Transactions

// Initialize
const { authorization_url, reference } = await paystack.transactions.initialize({
  email: '[email protected]',
  amount: 100000,
  currency: 'NGN',
  callback_url: 'https://yoursite.com/callback',
});

// Verify
const txn = await paystack.transactions.verify(reference);

// List with filters
const txns = await paystack.transactions.list({ status: 'success', perPage: 50 });

// Charge a returning customer with saved authorization
const charged = await paystack.transactions.chargeAuthorization({
  email: '[email protected]',
  amount: 100000,
  authorization_code: 'AUTH_xxx',
});

// Totals and export
const totals = await paystack.transactions.totals();
const { path } = await paystack.transactions.export({ status: 'success' });

Customers

// Create
const customer = await paystack.customers.create({
  email: '[email protected]',
  first_name: 'Ada',
  last_name: 'Obi',
});

// Fetch by customer code or email
const found = await paystack.customers.fetch('CUS_xxx');

// Update
await paystack.customers.update('CUS_xxx', { phone: '+2348012345678' });

// Flag a high-risk customer
await paystack.customers.setRiskAction({ customer: 'CUS_xxx', risk_action: 'deny' });

Plans

// Create a monthly plan
const plan = await paystack.plans.create({
  name: 'Pro Monthly',
  amount: 500000, // ₦5,000
  interval: 'monthly',
});

// List plans by interval
const plans = await paystack.plans.list({ interval: 'monthly' });

// Fetch by numeric ID or plan_code
const fetched = await paystack.plans.fetch('PLN_xxx');

Subscriptions

// Subscribe a customer to a plan
const sub = await paystack.subscriptions.create({
  customer: 'CUS_xxx',
  plan: 'PLN_xxx',
  authorization: 'AUTH_xxx',
});

// Enable / disable
await paystack.subscriptions.enable({ code: 'SUB_xxx', token: 'TOKEN_xxx' });
await paystack.subscriptions.disable({ code: 'SUB_xxx', token: 'TOKEN_xxx' });

// Generate a self-service management link
const { link } = await paystack.subscriptions.generateUpdateLink('SUB_xxx');

Transfers

// Single transfer
const transfer = await paystack.transfers.initiate({
  source: 'balance',
  amount: 200000, // ₦2,000
  recipient: 'RCP_xxx',
  reason: 'Vendor payout',
});

// Bulk transfers
const results = await paystack.transfers.bulk([
  { amount: 100000, recipient: 'RCP_aaa', reason: 'Invoice #1' },
  { amount: 150000, recipient: 'RCP_bbb', reason: 'Invoice #2' },
]);

// Finalize an OTP-protected transfer
await paystack.transfers.finalize({ transfer_code: 'TRF_xxx', otp: '123456' });

// Verify by reference
const verified = await paystack.transfers.verify('your-reference');

Transfer Recipients

// Create a NUBAN recipient
const recipient = await paystack.recipients.create({
  type: 'nuban',
  name: 'Jane Doe',
  account_number: '0123456789',
  bank_code: '058',
});

// Bulk create
const { success, errors } = await paystack.recipients.bulkCreate([
  { type: 'nuban', name: 'Alice', account_number: '0000000001', bank_code: '058' },
  { type: 'nuban', name: 'Bob',   account_number: '0000000002', bank_code: '011' },
]);

// Delete
await paystack.recipients.delete('RCP_xxx');

Subaccounts

// Create
const subaccount = await paystack.subaccounts.create({
  business_name: 'Acme Store',
  settlement_bank: '058',
  account_number: '0123456789',
  percentage_charge: 20,
});

// Fetch by ID or subaccount_code
const fetched = await paystack.subaccounts.fetch('ACCT_xxx');

// Update the percentage charge
await paystack.subaccounts.update('ACCT_xxx', { percentage_charge: 15 });

Splits

// Create a percentage split
const split = await paystack.splits.create({
  name: 'Marketplace Split',
  type: 'percentage',
  currency: 'NGN',
  subaccounts: [
    { subaccount: 'ACCT_aaa', share: 20 },
    { subaccount: 'ACCT_bbb', share: 30 },
  ],
});

// Add a subaccount
await paystack.splits.addSubaccount(split.id, { subaccount: 'ACCT_ccc', share: 10 });

// Remove a subaccount
await paystack.splits.removeSubaccount(split.id, { subaccount: 'ACCT_aaa' });

Refunds

// Full refund by transaction reference
const refund = await paystack.refunds.create({ transaction: 'ref_xxx' });

// Partial refund
const partial = await paystack.refunds.create({
  transaction: 'ref_xxx',
  amount: 25000,
  customer_note: 'Refund for returned item',
});

// List pending refunds
const pending = await paystack.refunds.list({ status: 'pending' });

Webhooks

import { verifyWebhookSignature, type WebhookEvent } from 'paystack-js';

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-paystack-signature'] as string;

  if (!verifyWebhookSignature(req.body, signature, process.env.PAYSTACK_SECRET_KEY!)) {
    return res.status(401).send('Unauthorized');
  }

  const event = JSON.parse(req.body.toString()) as WebhookEvent;

  switch (event.event) {
    case 'charge.success':
      // fulfil the order
      break;
  }

  res.sendStatus(200);
});

Error Handling

All Paystack API errors are thrown as a typed subclass of PaystackError. Catch broadly or narrow to a specific class:

import {
  PaystackError,
  PaystackAuthError,
  PaystackNotFoundError,
  PaystackValidationError,
  PaystackRateLimitError,
} from 'paystack-js';

try {
  const txn = await paystack.transactions.verify('invalid_ref');
} catch (err) {
  if (err instanceof PaystackAuthError) {
    // 401 — wrong or missing secret key
    console.error('Check your secret key');
  } else if (err instanceof PaystackNotFoundError) {
    // 404
    console.error('Not found:', err.paystackMessage);
  } else if (err instanceof PaystackValidationError) {
    // 400
    console.error('Validation error:', err.paystackMessage);
  } else if (err instanceof PaystackRateLimitError) {
    // 429 — also triggers automatic retry if maxRetries > 0
    console.error('Rate limited');
  } else if (err instanceof PaystackError) {
    // Any other Paystack API error
    console.error(`Error ${err.statusCode}:`, err.paystackMessage);
    console.error('Raw response:', err.raw);
  }
}

All PaystackError instances expose:

| Property | Type | Description | |---|---|---| | statusCode | number | HTTP status code from the response | | paystackMessage | string | Message field from the Paystack response body | | raw | unknown | The full raw response body |


Configuration

const paystack = new Paystack(process.env.PAYSTACK_SECRET_KEY!, {
  timeout: 10_000,
  maxRetries: 5,
});

| Option | Type | Default | Description | |---|---|---|---| | secretKey | string | — | Required. Your Paystack secret key (sk_test_... or sk_live_...) | | baseURL | string | 'https://api.paystack.co' | Override the Paystack API base URL | | timeout | number | 30000 | Request timeout in milliseconds | | maxRetries | number | 3 | Max automatic retry attempts on 429 and 5xx responses |

Retries use exponential backoff with jitter. If Paystack returns a Retry-After header (common on 429), that delay is respected exactly.


Webhook Verification

Paystack signs every webhook request with an HMAC-SHA512 digest of the raw request body, keyed with your secret key. The digest is sent in the x-paystack-signature header.

verifyWebhookSignature recomputes the HMAC and compares using Node's timingSafeEqual, which prevents timing attacks — a class of exploit where an attacker infers a valid signature by measuring how long the comparison takes.

Important: use express.raw() rather than express.json() to preserve the exact byte sequence Paystack signed. JSON re-serialisation changes whitespace and key ordering, producing a different hash.

import express from 'express';
import { verifyWebhookSignature, type WebhookEvent } from 'paystack-js';

const app = express();

app.post(
  '/webhook/paystack',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-paystack-signature'] as string;

    if (!verifyWebhookSignature(req.body, signature, process.env.PAYSTACK_SECRET_KEY!)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body.toString()) as WebhookEvent;

    switch (event.event) {
      case 'charge.success':
        // fulfil the order using event.data
        break;
      case 'transfer.success':
        // update payout records
        break;
      case 'subscription.create':
        // provision access
        break;
    }

    // Always respond 200 promptly — Paystack retries on non-2xx
    res.sendStatus(200);
  },
);

If you instantiated the SDK, paystack.webhooks.verifySignature(payload, signature) is equivalent and picks up the secret key from the instance automatically.


Contributing

git clone https://github.com/SCOnyema/paystack-js.git
cd paystack-js
pnpm install
pnpm test

Found a bug or missing a feature? Open an issue.


License

MIT © Samuel Onyema — see LICENSE.