Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .github/workflows/monnifytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jobs:
runs-on: ubuntu-latest

strategy:
max-parallel: 1
matrix:
node-version: [18.x, 20.x, 22.x, 24.x]

Expand All @@ -35,19 +36,27 @@ jobs:
- name: Run tests with coverage
env:
NODE_ENV: sandbox
MONNIFY_ENV: SANDBOX
BASEURL: https://sandbox.monnify.com
CONTRACT: ${{ secrets.MONNIFY_CONTRACT_CODE || '5867418298' }}
MONNIFY_APIKEY: ${{ secrets.MONNIFY_APIKEY || 'MK_TEST_GC3B8XG2XX' }}
MONNIFY_SECRET: ${{ secrets.MONNIFY_SECRET || 'A663NRZA544DDPEM7KDN7Z8HRV6YXD8S' }}
WALLETACCOUNTNUMBER: ${{ secrets.MONNIFY_WALLET_ACCOUNT || '3934178936' }}
TOKENEXPIRATIONTHRESHOLD: 500
TOKENFILE: Cache
run: npm run test:coverage
# c8 coverage instrumentation has a known incompatibility with Node 24's V8.
# Only run c8 on Node 22 (which also uploads to Codecov); other versions use plain mocha.
run: |
if [ "${{ matrix.node-version }}" = "22.x" ]; then
npm run test:coverage
else
npm test
fi

- name: Upload coverage to Codecov
# Only upload once — no point sending the same report from all 4 Node versions
if: matrix.node-version == '22.x'
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
Expand Down
40 changes: 25 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You need three things from your Monnify dashboard:

| Variable | Where to find it |
|---|---|
| `MONNIFY_ENV` | `SANDBOX` for testing, `LIVE` for production |
| `MONNIFY_APIKEY` | Dashboard → Settings → API Keys |
| `MONNIFY_SECRET` | Dashboard → Settings → API Keys |
| `CONTRACT` | Dashboard → Settings → Contract Code |
Expand All @@ -64,12 +65,15 @@ The recommended approach is a `.env` file so credentials never appear in source

```bash
# .env
MONNIFY_ENV=SANDBOX
MONNIFY_APIKEY=MK_TEST_XXXXXXXXXX
MONNIFY_SECRET=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
CONTRACT=1234567890
WALLETACCOUNTNUMBER=0123456789 # only needed for disbursement wallet balance
```

> The library reads `MONNIFY_ENV` automatically — no need to pass it to any constructor. It will also detect if your API key prefix does not match your declared environment (e.g. a `MK_PROD_` key with `MONNIFY_ENV=SANDBOX`) and throw a clear error.

Then load it at the top of your app:

```js
Expand All @@ -88,15 +92,15 @@ Every Monnify API call needs a **bearer token**. The library handles token fetch
```js
import { Transaction } from 'monnify-nodejs-lib';

const api = new Transaction('SANDBOX'); // or 'LIVE'
const api = new Transaction();

const [statusCode, token] = await api.getToken();
// token is the raw bearer string — pass it to every method call below
```

The token is cached in memory and reused until it expires, so calling `getToken()` multiple times is cheap.

> The environment value must be `'SANDBOX'` or `'LIVE'` (uppercase). All instances in the same process must use the same environment.
> The environment is read from `MONNIFY_ENV` in your environment variables. All instances in the same process must use the same environment.

---

Expand Down Expand Up @@ -128,7 +132,6 @@ import { MonnifyAPI } from 'monnify-nodejs-lib';
const monnify = new MonnifyAPI({
MONNIFY_APIKEY: process.env.MONNIFY_APIKEY,
MONNIFY_SECRET: process.env.MONNIFY_SECRET,
env: 'SANDBOX'
});

const [, token] = await monnify.getToken();
Expand All @@ -149,7 +152,7 @@ A reserved account is a **dedicated virtual bank account** assigned to a specifi
```js
import { ReservedAccount } from 'monnify-nodejs-lib';

const api = new ReservedAccount('SANDBOX');
const api = new ReservedAccount();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -196,7 +199,7 @@ Manage the full payment lifecycle — from initialising a checkout to charging a
```js
import { Transaction } from 'monnify-nodejs-lib';

const api = new Transaction('SANDBOX');
const api = new Transaction();
const [, token] = await api.getToken();
```

Expand All @@ -215,6 +218,13 @@ const [, token] = await api.getToken();
| `ThreeDsSecureAuthTransaction(token, data)` | Complete a 3DS card authentication |
| `cardTokenization(token, data)` | Charge a previously saved card token |

> **`from` / `to` date filters** — pass Unix timestamps in **milliseconds** (not ISO strings).
> ```js
> const to = Date.now();
> const from = to - 7 * 24 * 60 * 60 * 1000; // 7 days ago
> const [, resp] = await api.getAllTransactions(token, { from, to, page: 0, size: 20 });
> ```

### Example — Basic Payment Flow

```js
Expand Down Expand Up @@ -302,7 +312,7 @@ Split incoming payments automatically across multiple bank accounts.
```js
import { SubAccount } from 'monnify-nodejs-lib';

const api = new SubAccount('SANDBOX');
const api = new SubAccount();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -336,7 +346,7 @@ Create payment invoices with an expiry date and track their status.
```js
import { Invoice } from 'monnify-nodejs-lib';

const api = new Invoice('SANDBOX');
const api = new Invoice();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -377,7 +387,7 @@ Query how funds from transactions have been settled to your bank account.
```js
import { Settlement } from 'monnify-nodejs-lib';

const api = new Settlement('SANDBOX');
const api = new Settlement();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -411,7 +421,7 @@ Send money out of your Monnify wallet — single transfers, bulk transfers, and
```js
import { Disbursement } from 'monnify-nodejs-lib';

const api = new Disbursement('SANDBOX');
const api = new Disbursement();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -459,7 +469,7 @@ Reverse a payment back to the customer's original payment method.
```js
import { TransactionRefund } from 'monnify-nodejs-lib';

const api = new TransactionRefund('SANDBOX');
const api = new TransactionRefund();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -496,7 +506,7 @@ Check the available balance in your Monnify disbursement wallet.
```js
import { Wallet } from 'monnify-nodejs-lib';

const api = new Wallet('SANDBOX');
const api = new Wallet();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -526,7 +536,7 @@ Create and manage transaction limit profiles, then attach them to reserved accou
```js
import { LimitProfile } from 'monnify-nodejs-lib';

const api = new LimitProfile('SANDBOX');
const api = new LimitProfile();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -563,7 +573,7 @@ Set up recurring debit mandates — charge a customer's bank account on a schedu
```js
import { DirectDebit } from 'monnify-nodejs-lib';

const api = new DirectDebit('SANDBOX');
const api = new DirectDebit();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -631,7 +641,7 @@ getBillerCategories() → pick a category (e.g. "CABLE_TV")
```js
import { BillsPayment } from 'monnify-nodejs-lib';

const api = new BillsPayment('SANDBOX');
const api = new BillsPayment();
const [, token] = await api.getToken();
```

Expand Down Expand Up @@ -701,7 +711,7 @@ Validate bank accounts and verify customer identity documents.
```js
import { Verification } from 'monnify-nodejs-lib';

const api = new Verification('SANDBOX');
const api = new Verification();
const [, token] = await api.getToken();
```

Expand Down
39 changes: 23 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,35 @@ import { BillsPayment } from "./src/valueAddedService/billsPayment.js";


export class MonnifyAPI extends BaseRequestAPI {
constructor(config) {
process.env.MONNIFY_APIKEY = config.MONNIFY_APIKEY;
process.env.MONNIFY_SECRET = config.MONNIFY_SECRET;
super(config.env);
constructor(config = {}) {
if (config.MONNIFY_APIKEY) process.env.MONNIFY_APIKEY = config.MONNIFY_APIKEY;
if (config.MONNIFY_SECRET) process.env.MONNIFY_SECRET = config.MONNIFY_SECRET;
if (config.env && !process.env.MONNIFY_ENV) {
console.warn(
`[monnify] Passing env in the MonnifyAPI config is deprecated and will be removed in a future version. ` +
`Add MONNIFY_ENV=${config.env} to your .env file instead.`
);
process.env.MONNIFY_ENV = config.env;
}
super();

// ── Collections ──────────────────────────────────────────────────────
this.reservedAccount = new ReservedAccount(config.env);
this.transaction = new Transaction(config.env);
this.subAccount = new SubAccount(config.env);
this.invoice = new Invoice(config.env);
this.settlement = new Settlement(config.env);
this.limitProfile = new LimitProfile(config.env);
this.directDebit = new DirectDebit(config.env);
this.reservedAccount = new ReservedAccount();
this.transaction = new Transaction();
this.subAccount = new SubAccount();
this.invoice = new Invoice();
this.settlement = new Settlement();
this.limitProfile = new LimitProfile();
this.directDebit = new DirectDebit();

// ── Disbursements ────────────────────────────────────────────────────
this.disbursement = new Disbursement(config.env);
this.refund = new TransactionRefund(config.env);
this.wallet = new Wallet(config.env);
this.disbursement = new Disbursement();
this.refund = new TransactionRefund();
this.wallet = new Wallet();

// ── Value-added services ─────────────────────────────────────────────
this.verification = new Verification(config.env);
this.billsPayment = new BillsPayment(config.env);
this.verification = new Verification();
this.billsPayment = new BillsPayment();
}
}

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
".": "./index.js"
},
"scripts": {
"test": "mocha --timeout 15000 tests/index.test.js tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js tests/reservedAccount.test.js",
"test:coverage": "c8 --reporter=lcov --reporter=text mocha --timeout 15000 tests/index.test.js tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js tests/reservedAccount.test.js",
"test:core": "mocha --timeout 15000 tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js",
"test:new": "mocha --timeout 15000 tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js"
"test": "MONNIFY_ENV=SANDBOX mocha --timeout 15000 tests/index.test.js tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js tests/reservedAccount.test.js tests/env-validation.test.js",
"test:coverage": "MONNIFY_ENV=SANDBOX c8 --reporter=lcov --reporter=text mocha --timeout 15000 tests/index.test.js tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js tests/reservedAccount.test.js tests/env-validation.test.js",
"test:core": "MONNIFY_ENV=SANDBOX mocha --timeout 15000 tests/collection.test.js tests/refund.test.js tests/disbursement.test.js tests/subaccount.test.js tests/verification.test.js",
"test:new": "MONNIFY_ENV=SANDBOX mocha --timeout 15000 tests/wallet.test.js tests/invoice.test.js tests/settlement.test.js tests/billsPayment.test.js tests/limitProfile.test.js tests/directDebit.test.js"
},
"repository": {
"type": "git",
Expand Down
51 changes: 43 additions & 8 deletions src/base_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,56 @@ let _lockedEnvironment = null;
export class BaseRequestAPI {

constructor(environment) {
if (_lockedEnvironment && _lockedEnvironment !== environment) {
const envFromProcess = process.env.MONNIFY_ENV?.trim().toUpperCase();
const envFromArg = typeof environment === 'string' ? environment.trim().toUpperCase() : undefined;

let resolvedEnv;
if (envFromProcess) {
resolvedEnv = envFromProcess;
} else if (envFromArg) {
console.warn(
`[monnify] Passing the environment to the constructor is deprecated and will be removed in a future version. ` +
`Add MONNIFY_ENV=${envFromArg} to your .env file instead.`
);
resolvedEnv = envFromArg;
} else {
throw new Error(
`Environment conflict: already initialised as "${_lockedEnvironment}". ` +
`Cannot create a "${environment}" instance in the same runtime.`
`MONNIFY_ENV is not set. Add MONNIFY_ENV=SANDBOX or MONNIFY_ENV=LIVE to your environment variables.`
);
}

if (!['SANDBOX', 'LIVE'].includes(resolvedEnv)) {
throw new Error(
`Invalid environment "${resolvedEnv}". Must be "SANDBOX" or "LIVE".`
);
}
if (!['SANDBOX', 'LIVE'].includes(environment)) {

if (_lockedEnvironment && _lockedEnvironment !== resolvedEnv) {
throw new Error(
`Unknown environment "${environment}". Specify "SANDBOX" or "LIVE".`
`Environment conflict: already initialised as "${_lockedEnvironment}". ` +
`Cannot create a "${resolvedEnv}" instance in the same runtime.`
);
}

_lockedEnvironment = environment;
this.environment = environment;
this.baseUrl = environment === 'SANDBOX'
const apiKey = process.env.MONNIFY_APIKEY;
if (apiKey) {
if (resolvedEnv === 'SANDBOX' && apiKey.startsWith('MK_PROD_')) {
throw new Error(
`Environment mismatch: MONNIFY_ENV is "SANDBOX" but your API key starts with "MK_PROD_". ` +
`Use your sandbox key (starts with "MK_TEST_") or set MONNIFY_ENV=LIVE.`
);
}
if (resolvedEnv === 'LIVE' && apiKey.startsWith('MK_TEST_')) {
throw new Error(
`Environment mismatch: MONNIFY_ENV is "LIVE" but your API key starts with "MK_TEST_". ` +
`Use your live key (starts with "MK_PROD_") or set MONNIFY_ENV=SANDBOX.`
);
}
}

_lockedEnvironment = resolvedEnv;
this.environment = resolvedEnv;
this.baseUrl = resolvedEnv === 'SANDBOX'
? 'https://sandbox.monnify.com'
: 'https://api.monnify.com';
this.apiKey = process.env.MONNIFY_APIKEY;
Expand Down
2 changes: 1 addition & 1 deletion tests/base.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { BaseRequestAPI } from "../src/base_api.js";
let instance

beforeEach(async () =>{
instance = new BaseRequestAPI('SANDBOX')
instance = new BaseRequestAPI()
})


Expand Down
Loading