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

@norialabs/payments

v0.1.2

Published

Reusable TypeScript/JavaScript SDK for M-PESA Daraja, SasaPay, and Paystack payments.

Readme

@norialabs/payments

Reusable TypeScript/JavaScript SDK for M-PESA Daraja, SasaPay, and Paystack payments.

Current provider support:

  • M-PESA Daraja
  • SasaPay
  • Paystack

This package is provider-first. It gives you dedicated clients for each API instead of forcing providers into a lossy normalized abstraction too early.

Request payloads are strongly typed at compile time, but this first package cut does not perform full runtime schema validation of provider payloads. The SDK sends the payload object you provide after its internal normalization steps such as amount string conversion.

Scope

Implemented now:

  • M-PESA OAuth client credentials
  • M-PESA STK push
  • M-PESA STK push query
  • M-PESA C2B URL registration (v1 and v2)
  • M-PESA B2C
  • M-PESA B2B
  • M-PESA reversal
  • M-PESA transaction status query
  • M-PESA account balance query
  • M-PESA QR generation
  • SasaPay OAuth client credentials
  • SasaPay C2B request payment
  • SasaPay C2B OTP completion
  • SasaPay B2C
  • SasaPay B2B
  • SasaPay callback and IPN payload types
  • Paystack transaction initialize and verify
  • Paystack bank listing
  • Paystack account resolution
  • Paystack transfer recipient creation
  • Paystack transfer initiation, finalization, and verification
  • Environment-based client construction with fromEnv()
  • Paystack webhook signature and source-IP verification helpers

Not implemented yet:

  • SasaPay checkout payments
  • SasaPay remittance
  • SasaPay utilities
  • SasaPay WaaS
  • Daraja Bill Manager and portal-only APIs with undocumented request bodies

Source Docs

This SDK was implemented against:

Important SasaPay note:

  • Sandbox host is explicitly documented and is used as the default.
  • Production API host was not clearly documented in the reviewed docs, so this package requires baseUrl explicitly for SasaPay production instead of guessing.

Install

npm install @norialabs/payments

Runtime

  • Node.js >=20
  • ESM package output
  • Uses global fetch by default
  • You can inject your own fetch implementation if needed

Imports

Root exports:

import {
  ApiError,
  AuthenticationError,
  ClientCredentialsTokenProvider,
  ConfigurationError,
  PaystackClient,
  TimeoutError,
  WebhookVerificationError,
} from "@norialabs/payments";

Provider subpath exports:

import { MpesaClient, buildMpesaStkPassword, buildMpesaTimestamp } from "@norialabs/payments/mpesa";
import { PaystackClient } from "@norialabs/payments/paystack";
import { SasaPayClient } from "@norialabs/payments/sasapay";

Quick Start

M-PESA

import { MpesaClient, buildMpesaStkPassword, buildMpesaTimestamp } from "@norialabs/payments/mpesa";

const mpesa = new MpesaClient({
  consumerKey: process.env.MPESA_CONSUMER_KEY!,
  consumerSecret: process.env.MPESA_CONSUMER_SECRET!,
  environment: "sandbox",
});

const timestamp = buildMpesaTimestamp();

const response = await mpesa.stkPush({
  BusinessShortCode: "174379",
  Password: buildMpesaStkPassword({
    businessShortCode: "174379",
    passkey: process.env.MPESA_PASSKEY!,
    timestamp,
  }),
  Timestamp: timestamp,
  TransactionType: "CustomerPayBillOnline",
  Amount: 1,
  PartyA: "254700000000",
  PartyB: "174379",
  PhoneNumber: "254700000000",
  CallBackURL: "https://example.com/mpesa/callback",
  AccountReference: "INV-001",
  TransactionDesc: "Payment",
});

SasaPay

import { SasaPayClient } from "@norialabs/payments/sasapay";

const sasapay = new SasaPayClient({
  clientId: process.env.SASAPAY_CLIENT_ID!,
  clientSecret: process.env.SASAPAY_CLIENT_SECRET!,
  environment: "sandbox",
});

const response = await sasapay.requestPayment({
  MerchantCode: "600980",
  NetworkCode: "63902",
  Currency: "KES",
  Amount: "1.00",
  PhoneNumber: "254700000080",
  AccountReference: "12345678",
  TransactionDesc: "Request Payment",
  CallBackURL: "https://example.com/sasapay/callback",
});

Paystack

import { PaystackClient } from "@norialabs/payments/paystack";

const paystack = PaystackClient.fromEnv();

const response = await paystack.initializeTransaction({
  amount: 5000,
  email: "[email protected]",
  currency: "KES",
  reference: "INV-001",
  callback_url: "https://example.com/paystack/callback",
});

Shared Design

Authentication

By default, MpesaClient and SasaPayClient manage OAuth client-credentials tokens internally. PaystackClient uses your secret key directly as the bearer credential.

You can also inject your own token provider:

const tokenProvider = {
  async getAccessToken(forceRefresh?: boolean) {
    return process.env.ACCESS_TOKEN!;
  },
};

When a custom tokenProvider is supplied:

  • the SDK does not call the provider token endpoint
  • credentials like consumerKey or clientId are not required
  • you remain responsible for token freshness

Environment Configuration

All three provider clients support fromEnv():

const mpesa = MpesaClient.fromEnv();
const sasapay = SasaPayClient.fromEnv();
const paystack = PaystackClient.fromEnv();

Supported environment variables:

  • M-PESA: MPESA_CONSUMER_KEY, MPESA_CONSUMER_SECRET, MPESA_ENVIRONMENT, MPESA_BASE_URL, MPESA_TIMEOUT_SECONDS, MPESA_TOKEN_CACHE_SKEW_SECONDS
  • SasaPay: SASAPAY_CLIENT_ID, SASAPAY_CLIENT_SECRET, SASAPAY_ENVIRONMENT, SASAPAY_BASE_URL, SASAPAY_TIMEOUT_SECONDS, SASAPAY_TOKEN_CACHE_SKEW_SECONDS
  • Paystack: PAYSTACK_SECRET_KEY, PAYSTACK_BASE_URL, PAYSTACK_TIMEOUT_SECONDS

Amount Normalization

For request payloads that contain Amount, the SDK accepts string | number and serializes numbers to strings before sending the HTTP request.

Async Payment Behavior

Across all providers, many payment APIs are asynchronous.

Treat the initial API response as:

  • accepted
  • queued
  • in progress

Do not treat it as final settlement unless the provider explicitly says so. Final success or failure usually arrives via callback/webhook/IPN or a later query.

Response Pass-Through

Provider JSON responses are returned as raw provider payloads with typed known fields. If the provider includes additional keys beyond the documented interfaces, the SDK preserves them instead of stripping them.

Customization

This SDK is configurable at three levels:

  1. constructor-level transport and auth defaults
  2. per-request overrides
  3. hook-based request/response/error interception

Constructor Options

Shared transport-style options available on all clients:

| Option | Type | Default | Description | | --- | --- | --- | --- | | environment | "sandbox" \| "production" | "sandbox" | Selects the default base URL when documented. | | baseUrl | string | Provider default | Override the full provider base URL. | | fetch | typeof fetch | global fetch | Inject a custom fetch implementation. | | timeoutMs | number | undefined | Default timeout for all requests in milliseconds. | | tokenCacheSkewMs | number | 60000 | Refresh OAuth tokens slightly before expiry. Used by OAuth-backed clients only. | | defaultHeaders | HeadersInit | undefined | Headers added to every request. | | retry | RetryPolicy \| false | undefined | Default retry policy for all requests. | | hooks | HttpHooks | undefined | Request, response, and error hooks. |

Provider-specific auth options:

MpesaClient auth

Use one of:

| Auth mode | Required fields | | --- | --- | | Built-in OAuth | consumerKey, consumerSecret | | External token provider | tokenProvider |

SasaPayClient auth

Use one of:

| Auth mode | Required fields | | --- | --- | | Built-in OAuth | clientId, clientSecret | | External token provider | tokenProvider |

PaystackClient auth

Required fields:

| Auth mode | Required fields | | --- | --- | | Secret key | secretKey |

SasaPay Production

For SasaPay production you must pass:

const sasapay = new SasaPayClient({
  environment: "production",
  baseUrl: "https://your-confirmed-production-host/api/v1",
  clientId: process.env.SASAPAY_CLIENT_ID!,
  clientSecret: process.env.SASAPAY_CLIENT_SECRET!,
});

This is deliberate. The package does not guess an undocumented production host.

Per-Request Overrides

Every provider method accepts an optional second argument:

type ProviderRequestOptions = {
  headers?: HeadersInit;
  signal?: AbortSignal;
  timeoutMs?: number;
  retry?: RetryPolicy | false;
  accessToken?: string;
  forceTokenRefresh?: boolean;
};

Use it for:

  • request-specific headers
  • request-specific timeout
  • request-specific retry rules
  • direct access-token override
  • forced token refresh on the next call

Example:

await sasapay.requestPayment(payload, {
  timeoutMs: 15_000,
  headers: { "x-request-id": "abc-123" },
  retry: {
    maxAttempts: 2,
    retryMethods: ["POST"],
    retryOnStatuses: [500, 502, 503, 504],
    baseDelayMs: 250,
  },
});

Retry Policy

Retries are configurable but intentionally not enabled by default.

That default is deliberate for payments. Blind automatic retries on POST can duplicate operations unless you have upstream idempotency.

RetryPolicy fields:

| Field | Type | Description | | --- | --- | --- | | maxAttempts | number | Total attempts including the first request. | | retryMethods | HttpMethod[] | Restrict retries to certain methods such as ["GET"] or explicitly ["POST"]. | | retryOnStatuses | number[] | HTTP status codes that should trigger a retry. | | retryOnNetworkError | boolean | Retry on network/timeout style errors. | | baseDelayMs | number | Base delay before retrying. | | maxDelayMs | number | Upper bound for backoff delay. | | backoffMultiplier | number | Exponential multiplier applied to the base delay. | | shouldRetry | (context) => boolean \| Promise<boolean> | Final custom decision hook. |

Hooks

HttpHooks lets you observe and mutate request flow.

type HttpHooks = {
  beforeRequest?: Hook | Hook[];
  afterResponse?: Hook | Hook[];
  onError?: Hook | Hook[];
};

Hook context shapes:

| Hook | Context fields | | --- | --- | | beforeRequest | url, path, method, headers, body, attempt | | afterResponse | beforeRequest fields plus response, responseBody | | onError | beforeRequest fields plus error, optional response, optional responseBody |

Example:

const mpesa = new MpesaClient({
  consumerKey: "...",
  consumerSecret: "...",
  hooks: {
    beforeRequest(context) {
      context.headers.set("x-correlation-id", "corr-123");
    },
    afterResponse(context) {
      console.log(context.method, context.url, context.response.status);
    },
    onError(context) {
      console.error(context.method, context.url, context.error);
    },
  },
});

Header Precedence

Header merge order is:

  1. client defaultHeaders
  2. per-request headers
  3. SDK auth headers such as Authorization
  4. beforeRequest hook mutations

Errors

The package throws structured error classes:

| Error | When it is used | | --- | --- | | ConfigurationError | Invalid client configuration such as missing credentials or missing SasaPay production baseUrl. | | AuthenticationError | Token acquisition failed. | | TimeoutError | Request timeout elapsed. | | ApiError | Non-2xx provider response. | | WebhookVerificationError | Paystack webhook signature or source-IP verification failed. | | NoriapayError | Base class for package-specific errors. |

ApiError

ApiError includes:

  • message
  • code
  • status
  • responseBody

Example:

try {
  await mpesa.stkPush(payload);
} catch (error) {
  if (error instanceof ApiError) {
    console.error(error.status, error.responseBody);
  }
}

M-PESA

Base URLs

| Environment | Base URL | | --- | --- | | sandbox | https://sandbox.safaricom.co.ke | | production | https://api.safaricom.co.ke |

M-PESA Helpers

buildMpesaTimestamp(date?)

Formats a date into YYYYMMDDHHMMSS.

buildMpesaStkPassword({ businessShortCode, passkey, timestamp })

Returns base64(shortCode + passkey + timestamp).

M-PESA Client Methods

| Method | Endpoint | | --- | --- | | getAccessToken(forceRefresh?) | GET /oauth/v1/generate?grant_type=client_credentials | | stkPush(request, options?) | POST /mpesa/stkpush/v1/processrequest | | stkPushQuery(request, options?) | POST /mpesa/stkpushquery/v1/query | | registerC2BUrls(request, version?, options?) | POST /mpesa/c2b/v1/registerurl or POST /mpesa/c2b/v2/registerurl | | b2cPayment(request, options?) | POST /mpesa/b2c/v1/paymentrequest | | b2bPayment(request, options?) | POST /mpesa/b2b/v1/paymentrequest | | reversal(request, options?) | POST /mpesa/reversal/v1/request | | transactionStatus(request, options?) | POST /mpesa/transactionstatus/v1/query | | accountBalance(request, options?) | POST /mpesa/accountbalance/v1/query | | generateQrCode(request, options?) | POST /mpesa/qrcode/v1/generate |

M-PESA Response Types

Most non-STK M-PESA methods currently return MpesaApiResponse.

Known common fields:

| Field | Type | Meaning | | --- | --- | --- | | ConversationID | string | Provider conversation identifier. | | OriginatorConversationID | string | Originator-side conversation identifier. | | ResponseCode | string | Provider response code. "0" usually means accepted. | | ResponseDescription | string | Provider response description. | | CustomerMessage | string | Human-facing provider message when present. | | errorCode | string | Provider error code when present. | | errorMessage | string | Provider error message when present. |

For stkPush(), the SDK returns MpesaStkPushResponse, which extends MpesaApiResponse and may also include:

| Field | Type | | --- | --- | | MerchantRequestID | string | | CheckoutRequestID | string |

stkPush()

Request payload:

| Field | Type | Required | Notes | | --- | --- | --- | --- | | BusinessShortCode | string | Yes | Short code used for the transaction. | | Password | string | Yes | Usually from buildMpesaStkPassword(). | | Timestamp | string | Yes | Usually from buildMpesaTimestamp(). | | TransactionType | "CustomerPayBillOnline" \| "CustomerBuyGoodsOnline" | Yes | Daraja transaction type. | | Amount | string \| number | Yes | Normalized to string by the SDK. | | PartyA | string | Yes | Customer phone in international format. | | PartyB | string | Yes | Short code receiving payment. | | PhoneNumber | string | Yes | Customer phone. | | CallBackURL | string | Yes | Async callback endpoint. | | AccountReference | string | Yes | Merchant reference. | | TransactionDesc | string | Yes | Human-readable description. |

stkPushQuery()

Request payload:

| Field | Type | Required | | --- | --- | --- | | BusinessShortCode | string | Yes | | Password | string | Yes | | Timestamp | string | Yes | | CheckoutRequestID | string | Yes |

registerC2BUrls()

Request payload:

| Field | Type | Required | | --- | --- | --- | | ShortCode | string | Yes | | ResponseType | "Completed" \| "Cancelled" | Yes | | ConfirmationURL | string | Yes | | ValidationURL | string | Yes |

The optional version argument is:

  • "v2" by default
  • "v1" when you specifically need the older endpoint

b2cPayment()

Request payload:

| Field | Type | Required | | --- | --- | --- | | InitiatorName | string | Yes | | SecurityCredential | string | Yes | | CommandID | "BusinessPayment" \| "SalaryPayment" \| "PromotionPayment" | Yes | | Amount | string \| number | Yes | | PartyA | string | Yes | | PartyB | string | Yes | | Remarks | string | Yes | | QueueTimeOutURL | string | Yes | | ResultURL | string | Yes | | Occasion | string | No |

b2bPayment()

Request payload:

| Field | Type | Required | | --- | --- | --- | | Initiator | string | Yes | | SecurityCredential | string | Yes | | CommandID | "BusinessBuyGoods" \| "BusinessPayBill" \| "B2BAccountTopUp" | Yes | | Amount | string \| number | Yes | | PartyA | string | Yes | | PartyB | string | Yes | | Remarks | string | Yes | | AccountReference | string | Yes | | QueueTimeOutURL | string | Yes | | ResultURL | string | Yes |

reversal()

Request payload:

| Field | Type | Required | | --- | --- | --- | | Initiator | string | Yes | | SecurityCredential | string | Yes | | CommandID | "TransactionReversal" | Yes | | TransactionID | string | Yes | | Amount | string \| number | Yes | | ReceiverParty | string | Yes | | RecieverIdentifierType | string | Yes | | ResultURL | string | Yes | | QueueTimeOutURL | string | Yes | | Remarks | string | Yes | | Occasion | string | No |

Note: RecieverIdentifierType is intentionally spelled to match the source Daraja reference.

transactionStatus()

Request payload:

| Field | Type | Required | | --- | --- | --- | | Initiator | string | Yes | | SecurityCredential | string | Yes | | CommandID | "TransactionStatusQuery" | Yes | | TransactionID | string | Yes | | PartyA | string | Yes | | IdentifierType | string | Yes | | ResultURL | string | Yes | | QueueTimeOutURL | string | Yes | | Remarks | string | Yes | | Occasion | string | No |

accountBalance()

Request payload:

| Field | Type | Required | | --- | --- | --- | | Initiator | string | Yes | | SecurityCredential | string | Yes | | CommandID | "AccountBalance" | Yes | | PartyA | string | Yes | | IdentifierType | string | Yes | | ResultURL | string | Yes | | QueueTimeOutURL | string | Yes | | Remarks | string | Yes |

generateQrCode()

Request payload:

| Field | Type | Required | | --- | --- | --- | | MerchantName | string | Yes | | MerchantShortCode | string | Yes | | Amount | string \| number | Yes | | QRType | "PAYBILL" \| "BUYGOODS" | Yes |

SasaPay

Base URL

| Environment | Base URL behavior | | --- | --- | | sandbox | defaults to https://sandbox.sasapay.app/api/v1 | | production | must be supplied via baseUrl |

SasaPay Client Methods

| Method | Endpoint | | --- | --- | | getAccessToken(forceRefresh?) | GET /auth/token/?grant_type=client_credentials | | requestPayment(request, options?) | POST /payments/request-payment/ | | processPayment(request, options?) | POST /payments/process-payment/ | | b2cPayment(request, options?) | POST /payments/b2c/ | | b2bPayment(request, options?) | POST /payments/b2b/ |

SasaPay Authentication

The SDK uses:

  • GET /api/v1/auth/token/?grant_type=client_credentials
  • HTTP Basic auth using clientId:clientSecret
  • Bearer token for protected requests

requestPayment()

This is SasaPay C2B initiation.

Request payload:

| Field | Type | Required | Description | | --- | --- | --- | --- | | MerchantCode | string | Yes | Merchant code. | | NetworkCode | string | Yes | Payment channel identifier. | | Currency | string | Yes | Usually KES. | | Amount | string \| number | Yes | Normalized to string by the SDK. | | PhoneNumber | string | Yes | Customer phone number. | | AccountReference | string | Yes | Merchant transaction reference. | | TransactionDesc | string | Yes | Payment description. | | CallBackURL | string | Yes | Async callback URL. |

Documented response fields:

| Field | Type | Meaning | | --- | --- | --- | | status | boolean | Request status. | | detail | string | Provider detail message. | | PaymentGateway | string | Channel/provider used. | | MerchantRequestID | string | Merchant request identifier. | | CheckoutRequestID | string | SasaPay checkout identifier. | | TransactionReference | string | Unique payment request reference. | | ResponseCode | string | "0" usually means accepted. | | ResponseDescription | string | Provider response message. | | CustomerMessage | string | Customer-facing instructions. |

Network code behavior

From the reviewed SasaPay docs:

  • NetworkCode: "0" is SasaPay wallet and triggers an OTP flow
  • codes such as 63902 are mobile money providers like M-PESA and use an STK-like flow

processPayment()

Used for SasaPay wallet OTP completion only.

Request payload:

| Field | Type | Required | | --- | --- | --- | | MerchantCode | string | Yes | | CheckoutRequestID | string | Yes | | VerificationCode | string | Yes |

Response fields:

| Field | Type | | --- | --- | | status | boolean | | detail | string |

b2cPayment()

Request payload:

| Field | Type | Required | Description | | --- | --- | --- | --- | | MerchantCode | string | Yes | Merchant code. | | Amount | string \| number | Yes | Normalized to string by the SDK. | | Currency | string | Yes | Usually KES. | | MerchantTransactionReference | string | Yes | Merchant-side reference. | | ReceiverNumber | string | Yes | Receiver mobile or bank account number. | | Channel | string | Yes | Destination channel code. | | Reason | string | Yes | Payment reason. | | CallBackURL | string | Yes | Async callback URL. |

Documented response fields:

| Field | Type | | --- | --- | | status | boolean | | detail | string | | B2CRequestID | string | | ConversationID | string | | OriginatorConversationID | string | | ResponseCode | string | | TransactionCharges | string | | ResponseDescription | string |

b2bPayment()

Request payload:

| Field | Type | Required | | --- | --- | --- | | MerchantCode | string | Yes | | MerchantTransactionReference | string | Yes | | Currency | string | Yes | | Amount | string \| number | Yes | | ReceiverMerchantCode | string | Yes | | AccountReference | string | Yes | | ReceiverAccountType | "PAYBILL" \| "TILL" | Yes | | NetworkCode | string | Yes | | Reason | string | Yes | | CallBackURL | string | Yes |

Documented response fields:

| Field | Type | | --- | --- | | status | boolean | | detail | string | | B2BRequestID | string | | ConversationID | string | | OriginatorConversationID | string | | TransactionCharges | string | | ResponseCode | string | | ResponseDescription | string |

SasaPay Callback Types

The SDK exports callback payload types for downstream webhook handling.

SasaPayC2BCallback

Final C2B callback payload posted to your CallBackURL.

| Field | Type | | --- | --- | | MerchantRequestID | string | | CheckoutRequestID | string | | PaymentRequestID | string | | ResultCode | string | | ResultDesc | string | | SourceChannel | string | | TransAmount | string | | RequestedAmount | string | | Paid | boolean | | BillRefNumber | string | | TransactionDate | string | | CustomerMobile | string | | TransactionCode | string | | ThirdPartyTransID | string |

SasaPayC2BIpn

Instant payment notification payload.

| Field | Type | | --- | --- | | MerchantCode | string | | BusinessShortCode | string | | InvoiceNumber | string | | PaymentMethod | string | | TransID | string | | ThirdPartyTransID | string | | FullName | string | | FirstName | string | | MiddleName | string | | LastName | string | | TransactionType | string | | MSISDN | string | | OrgAccountBalance | string | | TransAmount | string | | TransTime | string | | BillRefNumber | string |

SasaPayTransferCallback

Shared callback shape for SasaPay B2C and B2B-style transfer result payloads.

| Field | Type | | --- | --- | | MerchantCode | string | | DestinationChannel | string | | RecipientName | string | | RecipientAccountNumber | string | | ResultCode | string | | ResultDesc | string | | SourceChannel | string | | SasaPayTransactionCode | string | | CheckoutRequestID | string | | SasaPayTransactionID | string | | ThirdPartyTransactionCode | string | | TransactionAmount | string | | TransactionCharge | string optional | | TransactionCharges | string optional | | MerchantRequestID | string | | MerchantTransactionReference | string | | TransactionDate | string | | MerchantAccountBalance | string | | LinkedTransactionCode | string optional |

Paystack

Base URL

  • default: https://api.paystack.co
  • override with baseUrl or PAYSTACK_BASE_URL

Paystack Client Methods

| Method | Endpoint | | --- | --- | | initializeTransaction(request, options?) | POST /transaction/initialize | | verifyTransaction(reference, options?) | GET /transaction/verify/{reference} | | listBanks(query?, options?) | GET /bank | | resolveAccount({ accountNumber, bankCode }, options?) | GET /bank/resolve | | createTransferRecipient(request, options?) | POST /transferrecipient | | initiateTransfer(request, options?) | POST /transfer | | finalizeTransfer(request, options?) | POST /transfer/finalize_transfer | | verifyTransfer(reference, options?) | GET /transfer/verify/{reference} |

Paystack Notes

  • amounts are lowest-unit integers, so 5000 means 50.00 in a 2-decimal currency such as KES
  • accessToken in per-request options overrides the bearer secret key for one request
  • Paystack does not use OAuth token lookup in this SDK

Verify Paystack Webhooks

import {
  PAYSTACK_WEBHOOK_IPS,
  requirePaystackSignature,
  requireSourceIp,
} from "@norialabs/payments";

export function verifyPaystackWebhook(rawBody, signature, sourceIp) {
  requirePaystackSignature(rawBody, signature, process.env.PAYSTACK_SECRET_KEY!);
  requireSourceIp(sourceIp, PAYSTACK_WEBHOOK_IPS);
}

Usage Patterns

Use the SDK-managed token flow

Best when:

  • you want the package to fetch and cache OAuth tokens
  • you do not already have a central auth service

Use an external token provider

Best when:

  • you already manage tokens elsewhere
  • you want a shared token cache across multiple SDK clients
  • you need custom auth behavior not built into the package

Example:

import { MpesaClient } from "@norialabs/payments/mpesa";

const client = new MpesaClient({
  tokenProvider: {
    async getAccessToken(forceRefresh) {
      return getTokenFromMyCache(forceRefresh);
    },
  },
});

Override token per request

Best when:

  • a single call must use a specific bearer token
  • you are proxying requests through your own auth layer
await sasapay.processPayment(payload, {
  accessToken: "pre-fetched-token",
});

Force a fresh token

await mpesa.stkPush(payload, {
  forceTokenRefresh: true,
});

Versioning Expectations

This is the first package cut. Expect additive growth in:

  • more provider endpoints
  • richer response typing
  • more provider-specific modules

Breaking changes should be reserved for places where the current first-cut API is too weak or misleading.