Skip to content

SCOnyema/paystack-js

Repository files navigation

paystack-js

A fully-typed TypeScript SDK for the Paystack API

npm version npm downloads 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: 'customer@example.com',
  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: 'customer@example.com',
  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: 'customer@example.com',
  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: 'customer@example.com',
  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.

About

Fully-typed TypeScript SDK for Paystack. Covers transactions, subscriptions, plans, splits, and transfers.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors