A fully-typed TypeScript SDK for the Paystack API
- Full TypeScript types for every request param, response object, and error class
- Dual CJS + ESM output — works with
requireorimport, ships.d.tsdeclarations 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.databoilerplate - Typed error classes — catch
PaystackAuthError,PaystackNotFoundError,PaystackValidationError, etc. by type - Webhook signature verification using HMAC-SHA512 and
timingSafeEqual
npm install paystack-jspnpm add paystack-jsyarn add paystack-jsRequires Node.js ≥ 18.
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}`);
}// 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' });// 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' });// 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');// 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');// 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');// 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');// 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 });// 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' });// 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' });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);
});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 |
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.
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.
git clone https://github.com/SCOnyema/paystack-js.git
cd paystack-js
pnpm install
pnpm testFound a bug or missing a feature? Open an issue.
MIT © Samuel Onyema — see LICENSE.