Guidance for AI coding agents working in this repository.
- Package:
paystack-sdk(published to npm), currently v4.0.0 - Purpose: Promise-based, fully typed TypeScript SDK for the Paystack payments REST API (
https://api.paystack.co) - License: MIT — Author: Tech Priest (Asaju Enitan)
- Repository: https://github.com/tekpriest/paystack-node
- Runtime dependency: none. HTTP is handled by the built-in global
fetch(seesrc/http.ts); everything else is dev tooling. - Minimum Node.js version: 18 (global
fetch).
The SDK is a thin, typed wrapper over Paystack's HTTP API. It does no business logic, validation, caching, or retries of its own — it serializes typed parameters into HTTP calls and returns the API's JSON envelope.
| Command | What it does |
|---|---|
npm ci / npm install |
Install dependencies (npm is authoritative; CI uses npm ci) |
npm run build |
Compile TypeScript with tsc into dist/ (includes .d.ts declarations) |
npm test |
Run Jest (ts-jest preset) |
npm run lint |
ESLint 8 + @typescript-eslint over all .ts files |
npm run format |
prettier --write "src/**/*.ts" |
prepare→npm run build(sodist/is built on publish;dist/is git-ignored)prepublishOnly→npm run lintpreversion→ format + lint +git add -A srcpostversion→git push && git push --tags
Classic facade pattern:
-
src/paystack.tsdefines thePaystackclass — the single entry point users instantiate with their secret key:const paystack = new Paystack('sk_live_...');
-
The constructor creates one shared HTTP client via
createHttpClient(key)(defined insrc/http.ts, built on the globalfetch). It targetshttps://api.paystack.co, sendsAuthorization: Bearer <key>andContent-Type: application/json, and unwraps each response to its parsed JSON body — so module methods resolve directly to the Paystack JSON envelope ({ status, message, data }), not the raw HTTP response. -
Each Paystack API area is a module class in its own directory under
src/. ThePaystackconstructor instantiates one of each, passing the sharedHttpClientinstance, and exposes it as a public property:Property on PaystackDirectory Class bulkchargesrc/bulkcharge/BulkChargechargesrc/charge/Chargecustomersrc/customer/Customerdedicatedsrc/dedicated/DedicatedAccountdirectDebitsrc/directdebit/DirectDebitdisputesrc/dispute/Disputeintegrationsrc/integration/Integrationinvoicesrc/invoice/Invoicemiscsrc/misc/Misc(banks, countries, states)pagesrc/payment/PaymentPagepaymentRequestsrc/paymentrequest/PaymentRequestplansrc/plan/Planproductsrc/product/Productrecipientsrc/recipient/Recipientrefundsrc/refund/Refundsettlementsrc/settlement/Settlementsplitsrc/split/TransactionSplitsubAccountsrc/subaccounts/SubAccountsubscriptionsrc/subscription/Subscriptionterminalsrc/terminal/Terminaltransactionsrc/transaction/Transactiontransfersrc/transfer/Transfer(also exposestransfer.control— a nestedControlclass fromsrc/transfer/control.tsfor balance/OTP endpoints)applePaysrc/apple/ApplePayverificationsrc/verification/VerificationvirtualTerminalsrc/virtualterminal/VirtualTerminal -
src/index.tsis the package entry point:export = Paystack(CommonJS single-value export, soimport Paystack from 'paystack-sdk'works from both CJS and ESM/nodenextconsumers).
Each module directory follows the same layout:
interface.ts— all request/response TypeScript types for that API area<name>.ts— the module classindex.ts— re-export barrel (export * from './interface'; export * from './<name>';) — only present in some modules (plan/,product/,subscription/;src/dedicated/index.tsexists but is empty and unused)
Shared types live in src/interface.ts:
Meta(pagination:total,skipped,perPage,page,pageCount)BadRequestandResponse(Paystack envelope shapes)QueryParams(perPage,page,from,to) — base interface for list methodsCurrencyunion:'NGN' | 'USD' | 'GHS' | 'ZAR' | 'KES'
Module class conventions (follow these when adding endpoints):
- Constructor takes
http: HttpClientand stores it asprivate http: HttpClient - Methods are
async, take typed parameters, and returnPromise<SpecificResponse | BadRequest> - GET with query string:
this.http.get('/path', { params: { ...queryParams } }) - POST/PUT bodies:
this.http.post('/path', JSON.stringify(data)) - Import shared types (
BadRequest,Response,QueryParams,Meta) from../interface. - Type naming: request types are named after the operation (
InitializeTransaction,CreateCharge,UpdateCustomer); response types end inResponse,Created,Initiated, etc. (ListTransactionsResponse,TransferInitiated). Field names inside interfaces are snake_case to match Paystack's JSON. - Amounts are in currency subunits (kobo for NGN, pesewas for GHS, cents for ZAR). The SDK does no conversion; in several interfaces
amountis typed asstring. - JSDoc comments on public methods, often with markdown headings. (Note: one existing heading in
src/transaction/transaction.tsmisspells "Transactions" as "Tansactions" — don't copy that.)
- Create
src/<area>/interface.tswith the request/response types, andsrc/<area>/<area>.tswith the class following the conventions above. - Wire it into
src/paystack.ts: import the class, declare apublicproperty, and instantiate it in the constructor withthis.http. - Add the module to the README's "Supported Modules" table.
- Language: all code, comments, docs, and commit messages are in English.
- Formatting: Prettier 2.5.1 with
printWidth: 80,trailingComma: "all",singleQuote: true(see.prettierrc). The codebase currently passesprettier --check. Runnpm run formatbefore committing. - Linting: ESLint 8.6.0 with
eslint:recommended+plugin:@typescript-eslint/recommended(.eslintrc.js). One project-specific rule:no-restricted-importswarns against the named importimport { Paystack } from 'paystack-sdk'— the default import is the only supported API (the named export was removed in v4.0.0, so this rule is now vestigial). - TypeScript:
strict: true, targetES2021, CommonJS modules, declaration files emitted todist/.tsconfig.jsoncompiles onlysrc/**/*.tsand excludes**/__tests__/*. - Dead config:
tslint.jsonis legacy (tslint is not installed) andjestconfig.jsonis an older Jest config superseded byjest.config.ts(ts-jest preset, node test environment). Leave both alone unless you're removing them. - Lockfiles: both
package-lock.jsonandbun.lockare tracked;package-lock.jsonis the current one (CI usesnpm ci). Keeppackage-lock.jsonin sync when changing dependencies. - Git history: commits loosely follow Conventional Commits (
feat:,fix:,docs:,Bump ...for dependabot PRs); version releases are committed as bare version tags (e.g.4.0.0) with the version bumped inpackage.json. KeepCHANGELOG.mdupdated when cutting a release.
- Tests live under
src/__tests__/:http.test.ts(unit tests, mockedfetch) andintegration.test.ts(live API, skipped unlessPAYSTACK_TEST_KEYis set). The active Jest config isjest.config.ts(preset: 'ts-jest',testEnvironment: 'node'). - When writing tests, use ts-jest default conventions (
__tests__/or*.test.ts/*.spec.ts) so both Jest and thetscbuild exclude rules apply. - You can smoke-test the built SDK with
npm run buildfollowed by a small Node script requiring./dist(e.g. stubglobalThis.fetchto avoid real network calls — the constructor only accepts a key string, so a fake key likesk_test_...works for instantiation).
- Publishing ships only
dist/**/*(package.jsonfilesfield);mainisdist/index.js.package.jsonalso declares explicittypesand anexportsmap (".": { types, default }) pointing atdist/index.d.ts/dist/index.js. Note theexportsmap blocks deep imports (e.g.paystack-sdk/dist/...); only the package root is public. - Release flow (intended): bump version →
preversionformats + lints →postversionpushes commit and tags →npm publishrunsprepare(build) andprepublishOnly(lint). - CI:
.github/workflows/publish.ymlruns on push tomain:npm ci→npm test→npm run build→ version-exists check →npm publish --access public(requires theNPM_TOKENsecret) → create a GitHub release.
- The SDK is designed for server-side use only. The constructor key (
sk_live_.../sk_test_...) is the Paystack secret key and is sent as aBearertoken on every request. Never commit keys, log them, or ship this client to browser code. .envfiles are git-ignored.- The SDK performs no validation or sanitization of inputs; Paystack's API is the validator. Keep that in mind when changing method signatures — preserving the typed contracts (
interface.tsfiles) is the SDK's core value. - Non-2xx responses reject with a
PaystackHttpError(carrying.statusand the parsed.body); other errors propagate to the caller unmodified. - There are no runtime dependencies; HTTP uses the Node.js built-in
fetch(Node 18+), so there is nothing to pin or receive dependabot bumps.