payaza-node-sdk
v0.1.1
Published
Official Node.js SDK for the Payaza API — collect payments and send transfers across Africa.
Maintainers
Readme
payaza-node-sdk
Official Node.js / TypeScript SDK for the Payaza API — collect payments (cards, virtual accounts, mobile money) and send transfers across Africa from your server.
- Typed end-to-end: every request and response shape ships as a TypeScript type.
- Works in both
require()andimportprojects (CJS + ESM builds). - Handles Payaza's auth scheme for you — pass your raw API key, the SDK base64-encodes it and attaches the right headers per call.
- Zero runtime dependencies — built on Node's native
fetch.
Status: v0.1.0 covers the full Payaza API surface — Accounts (main account, sub-accounts, EUR accounts, virtual accounts, branches), Transfers, Mobile Money/XOF/ZAR Collections, Card Collections, Authorize/Capture/Void, Refunds & Chargebacks, Subscriptions, and Split Settlements. See API coverage below for the full resource list.
Requirements
- Node.js 18 or later (for native
fetchandWeb Crypto). - A Payaza business account with API keys — see Getting your API keys.
Installation
npm install payaza-node-sdkyarn add payaza-node-sdkpnpm add payaza-node-sdkGetting your API keys
- Sign up / log in at business.payaza.africa.
- Go to Settings → Developers.
- Click Generate Keys. Toggle Test Mode to get a sandbox key, or use live keys once your KYB verification is approved.
Keep your key on the server — never ship it in client-side / mobile code. If a key is ever exposed, regenerate it immediately from the same screen.
Quick start
import { Payaza } from "payaza-node-sdk";
const payaza = new Payaza({
publicKey: process.env.PAYAZA_PUBLIC_KEY!,
environment: "test", // "test" while integrating, "live" once you go live
});
const account = await payaza.account.view();
console.log(account.data);// CommonJS also works out of the box
const { Payaza } = require("payaza-node-sdk");
const payaza = new Payaza({ publicKey: process.env.PAYAZA_PUBLIC_KEY, environment: "test" });Client options
new Payaza({
publicKey: "...", // required — your raw Payaza public key (not pre-encoded)
environment: "test", // "test" | "live", defaults to "test". Sent as X-TenantID on endpoints that require it.
productId: "app", // sent as X-ProductID, only on the Mobile Money collection endpoints that require it.
baseUrl: "https://api.payaza.africa/live", // override for testing against a mock server
timeoutMs: 30_000, // per-request timeout
});Two things that trip people up on every Payaza integration, handled for you:
- The
/livepath segment is fixed. It's part of the API host regardless of whether you're in test or live mode — yourenvironmentoption (viaX-TenantID) is what actually switches modes, not the URL. - The
Authorizationheader needs your key base64-encoded, with aPayazaprefix instead ofBearer. Just pass the raw key from the dashboard aspublicKey— the SDK encodes it on every request.
Usage
Account
// All currency sub-accounts under your business, with balances and account references
const { data: accounts } = await payaza.account.view();
// Look up a transaction by the reference you supplied
const status = await payaza.account.getTransactionStatus("TD93001234");
// Supported banks / mobile money providers for a currency (gives you bank_code values)
const { data: banks } = await payaza.account.getBankCodes("NGN");
// Resolve an account number + bank code to an account holder's name before transferring
const enquiry = await payaza.account.nameEnquiry({
service_payload: {
currency: "NGN",
bank_code: "100004",
account_number: "0190878999",
},
});
console.log(enquiry.response_content.account_name);Transfers
const transfer = await payaza.transfers.initiate({
transaction_type: "nuban", // NGN. Use "mobile_money" for GHS/UGX/KES/etc — see field docs below
service_payload: {
payout_amount: 100,
transaction_pin: 490736, // 6-digit PIN set on the dashboard
account_reference: "1010000009", // from payaza.account.view()
currency: "NGN",
payout_beneficiaries: [
{
credit_amount: 100,
account_number: "9207067319",
account_name: "John Doe",
bank_code: "000013",
narration: "Invoice #482", // <= 25 chars, no special characters
transaction_reference: "TD93001234", // unique, >= 10 characters
sender: {
sender_name: "Jane Doe",
sender_phone_number: "01234595",
sender_address: "123, Ace Street",
},
},
],
},
});
// Later — check how it landed
const status = await payaza.transfers.getStatus("TD93001234");transaction_type selects the payout rail per currency: nuban for NGN, mobile_money (or a country-specific rail like ghipps, kepss, tiss, wave) for GHS/UGX/KES/TZS/XOF, and RTC for ZAR. The sum of every beneficiary's credit_amount must equal payout_amount, and every transaction_reference you send must be globally unique — reusing one is rejected by the API.
Sub-accounts
Segmented accounts under your main Payaza account, for merchants, vendors, or internal business units.
const created = await payaza.subAccounts.create({
mainAccountPayazaReference: "100000000", // your main account's reference, from payaza.account.view()
name: "Test Sub Account",
currency: "NGN",
country: "NGA", // ISO 3166-1 alpha-3
});
const subAccount = await payaza.subAccounts.get(created.data[0].payazaAccountReference);EUR accounts
EUR-denominated sub-accounts for corporates or individuals — each request goes through document review before the account is provisioned.
// Check which ID / proof-of-address documents are accepted for the applicant's country first
const { data: acceptableIds } = await payaza.eurAccounts.acceptableIds();
// Individual applicant
const request = await payaza.eurAccounts.requestUser({
country: "US",
currency: "EUR",
category: "SUB_ACCOUNT",
purpose: "Freelance invoicing",
consent: true,
account_type: "user",
main_account_payaza_reference: "10987654312",
id_file: "https://your-cdn.example.com/uploads/passport.jpg",
id_type: "passport",
poa_file: "https://your-cdn.example.com/uploads/utility-bill.png",
poa_type: "utilityBill",
first_name: "Steve",
last_name: "Stones",
});
// Company applicant — payaza.eurAccounts.requestCorporate({ ... company_name, certificate_of_incorporation, directors: [...] })
// Track review status
const status = await payaza.eurAccounts.getRequest(request.creationReference);
// All requests you've submitted, or a specific category
const requests = await payaza.eurAccounts.listRequests({ category: "SUB_ACCOUNT" });
// Approved/active EUR sub-accounts (paginated)
const subAccounts = await payaza.eurAccounts.list({ pageSize: 10, pageNumber: 1, currency: "EUR" });Virtual accounts
Collect NGN bank transfers into a Dynamic (single-use, amount-locked, auto-expiring) or Static (reusable) virtual account.
const dynamic = await payaza.virtualAccounts.create({
account_type: "Dynamic",
account_name: "Test DVA",
bank_code: "140", // Globus Bank
account_reference: "accRef123", // unique per account/transaction
customer_first_name: "John",
customer_last_name: "Doe",
customer_email: "[email protected]",
transaction_amount: "1000",
expires_in_minutes: "20",
});
console.log(dynamic.data.account_number);
// Static accounts require a validated BVN
const reserved = await payaza.virtualAccounts.create({
account_type: "Static",
account_name: "Test Reserved VA",
bank_code: "140",
bvn: "323212345",
bvn_validated: true,
account_reference: "accRef456",
customer_first_name: "John",
customer_last_name: "Doe",
customer_email: "[email protected]",
});
const accountStatus = await payaza.virtualAccounts.getStatus(dynamic.data.account_number);
const txnStatus = await payaza.virtualAccounts.getTransactionStatus("accRef123");
// Test mode only — simulate an inbound transfer instead of moving real money
await payaza.virtualAccounts.fundTestAccount({
account_name: dynamic.data.account_name,
account_number: dynamic.data.account_number,
initiation_transaction_reference: "accRef123", // required for Dynamic accounts, omit for Static
transaction_amount: "1000",
currency: "NGN",
source_account_number: "0123456789",
source_account_name: "Jill Stones",
source_bank_name: "Test Bank",
});Branches
const { data: branches } = await payaza.branches.list();Webhooks
Payaza signs every webhook with HMAC-SHA512 over the raw request body, sent in the x-payaza-signature header. Verify it before trusting the payload:
import { verifyWebhookSignature } from "payaza-node-sdk";
import express from "express";
const app = express();
app.post(
"/webhooks/payaza",
express.raw({ type: "application/json" }), // keep the body raw/unparsed for verification
(req, res) => {
const signature = req.header("x-payaza-signature") ?? "";
const isValid = verifyWebhookSignature(req.body, signature, process.env.PAYAZA_WEBHOOK_SECRET!);
if (!isValid) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString("utf-8"));
// Use event.transaction_reference (or equivalent) as an idempotency key —
// Payaza may redeliver the same event.
res.sendStatus(200);
},
);Configure your webhook URLs (separate ones for test and live) under Settings → Developers → Webhooks on the dashboard.
Error handling
Every failure — a non-2xx API response, a network error, or a timeout — raises PayazaError:
import { Payaza, PayazaError } from "payaza-node-sdk";
try {
await payaza.transfers.initiate({ /* ... */ });
} catch (error) {
if (error instanceof PayazaError) {
console.error(error.message); // human-readable message from the API (or the SDK, for network/timeout errors)
console.error(error.status); // HTTP status code, when the failure came from the API
console.error(error.response); // full parsed response body, for logging/debugging
}
throw error;
}Common causes are listed in the Payaza errors reference — e.g. mismatched test/live keys, reused transaction_references, or an incorrect transaction PIN.
API coverage
The SDK is being built out incrementally against Payaza's full API surface. Everything below will follow the same payaza.<resource>.<method>() shape shown above.
| Resource | Status | Notes |
|---|---|---|
| payaza.account | ✅ Available | View details, transaction status, bank codes, name enquiry |
| payaza.transfers | ✅ Available | Initiate transfer, check status |
| payaza.subAccounts | ✅ Available | Create / view sub-accounts |
| payaza.eurAccounts | ✅ Available | EUR sub-account requests, acceptable IDs, approved sub-accounts |
| payaza.virtualAccounts | ✅ Available | Static/dynamic virtual accounts, status, test funding |
| payaza.branches | ✅ Available | List merchant branches |
| payaza.mobileMoneyCollections | ✅ Available | Mobile money / XOF / ZAR collections, OTP flow, transaction status, test funding |
| payaza.cards | ✅ Available | Card charge, transaction/refund status |
| payaza.authCaptureVoid | ✅ Available | Authorize, capture, void card transactions; list/get authorizations |
| payaza.refunds | ✅ Available | Initiate refund, refund history |
| payaza.chargebacks | ✅ Available | List, accept/reject, transaction history |
| payaza.subscriptionPlans / payaza.subscriptions | ✅ Available | Plans, lifecycle (pause/resume/cancel/change plan), on-demand charging, invoices |
| payaza.splitSettlements | ✅ Available | Split-account CRUD |
| verifyWebhookSignature | ✅ Available | Standalone helper, no client instance needed |
payaza.mobileMoneyCollections also covers what was previously listed separately as subsidiaryCollections — Payaza exposes MoMo, XOF, and ZAR collections through the same API section.
Full endpoint-level reference: docs.payaza.africa/api-reference.
TypeScript
Types are generated as part of the build (dist/index.d.ts / dist/index.d.cts) and published with the package — no @types/ package needed. Every resource method's request and response shape is exported from the package root:
import type { InitiateTransferRequest, PayazaMainAccount } from "payaza-node-sdk";Development
git clone https://github.com/78-Financials/payaza-node-sdk.git
cd payaza-node-sdk
npm install
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run test # vitest
npm run build # tsup — emits dist/ (ESM + CJS + .d.ts)Support
- Docs: docs.payaza.africa
- Discord: discord.gg/976qrMNF6
- Slack: payaza-community.slack.com
- Email: [email protected]
- SDK issues: open one on this repository
License
MIT
