@kuvarpay/sdk 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -95,8 +95,8 @@ The SDK defaults to the KuvarPay Production API (`https://payment.kuvarpay.com`)
95
95
 
96
96
  | Method | Description |
97
97
  | :--- | :--- |
98
- | `getBanks({ country? \| currency?, type? })` | List a corridor's **primary-rail** payment methods (banks / mobile money). Pass a country (ISO-2) or currency (ISO-4217). |
99
- | `resolveBankAccount(data)` | Verify bank account names before creating subaccounts. |
98
+ | `getBanks({ country? \| currency?, type?, direction?, sandbox? })` | List a corridor's **primary-rail** payment methods (banks / mobile money). Pass a country (ISO-2) or currency (ISO-4217). Set `sandbox: true` when your key is a sandbox-mode business — live and sandbox are separate `methodId` spaces. |
99
+ | `resolveBankAccount(data)` | **Deprecated** alias for `resolveRecipient` — takes `methodId`, not `bank_code`. |
100
100
 
101
101
  ### 🌍 Fiat Collection (Pay with Local Currency)
102
102
 
@@ -104,7 +104,7 @@ The SDK defaults to the KuvarPay Production API (`https://payment.kuvarpay.com`)
104
104
  | :--- | :--- |
105
105
  | **`createFiatPayment(data)`** | **One-shot**: checkout session + fiat transaction + wait for the deposit. Returns `{ sessionId, transactionId, depositId, nextAction, quote, paymentInstructions }`. |
106
106
  | `getFiatCountries()` | Supported corridors. `code` is a **currency**; use **`countryCode`** (ISO-2) for any `country` param, and filter on `depositAvailable`. |
107
- | `getFiatMethods({ country \| currency, type? })` | Banks / mobile-money operators. Takes ISO-2 `country` **or** ISO-4217 `currency`. Each entry carries a `methodId`. Unknown corridor → `{ methods: [] }`, not an error. |
107
+ | `getFiatMethods(...)` | **Deprecated** alias for `getBanks` — same endpoint, same params. |
108
108
  | `getFiatCheckoutQuote({ amount, baseCurrency, currency, country })` | Preview the local amount a customer pays. |
109
109
  | `createFiatTransaction(data)` | Create a fiat collection on a checkout session. |
110
110
  | `waitForFiatDeposit(sessionId, options?)` | Bounded poll until the deposit attaches. Rejects with `FIAT_DEPOSIT_TIMEOUT`. |
@@ -117,7 +117,7 @@ the customer's payment prompt, and the gateway withholds `paymentInstructions`
117
117
  until then. Show `quote`, confirm, then show instructions.
118
118
 
119
119
  ```javascript
120
- const { methods } = await kv.getFiatMethods({ country: 'KE', type: 'mobile_money' });
120
+ const { methods } = await kv.getBanks({ country: 'KE', type: 'mobile_money' });
121
121
 
122
122
  const payment = await kv.createFiatPayment({
123
123
  amount: 5000, currency: 'NGN', // you settle in NGN
@@ -172,14 +172,14 @@ Payouts move money **out** of your USD balance to a recipient's bank or mobile-m
172
172
 
173
173
  | Method | Description |
174
174
  | :--- | :--- |
175
- | `getFiatMethods({ country, type?, direction: 'outbound' })` | List payout (outbound) banks / mobile-money operators. |
175
+ | `getBanks({ country, type?, direction: 'outbound' })` | List payout (outbound) banks / mobile-money operators. |
176
176
  | `resolveRecipient({ methodId, accountNumber?, phone? })` | Name-enquiry on a payout recipient before sending. |
177
177
  | `createFiatSend(data)` | Initiate a bank / mobile-money payout (USD-debited). |
178
178
  | `getFiatSend(sendId)` | Poll a payout's status (`PROCESSING` → `COMPLETED`/`FAILED`/`REFUNDED`). |
179
179
 
180
180
  ```javascript
181
181
  // Discover payout methods, then verify the recipient name before sending.
182
- const { methods } = await kv.getFiatMethods({ country: 'NG', type: 'bank', direction: 'outbound' });
182
+ const { methods } = await kv.getBanks({ country: 'NG', type: 'bank', direction: 'outbound' });
183
183
  const recipient = await kv.resolveRecipient({ methodId: methods[0].methodId, accountNumber: '0123456789' });
184
184
 
185
185
  // Fiat payout: recipient receives exactly 50,000 NGN; USD debited from your balance.
package/index.d.ts CHANGED
@@ -2,6 +2,12 @@ export interface KuvarPayServerConfig {
2
2
  baseUrl?: string;
3
3
  businessId?: string;
4
4
  secretApiKey?: string;
5
+ /**
6
+ * Set true when the key belongs to a sandbox-mode business. Live and sandbox
7
+ * are separate `methodId` spaces, so `getBanks()` must be fetched in the same
8
+ * mode the key resolves/pays out in. Defaults to `KUVARPAY_SANDBOX=true`.
9
+ */
10
+ sandbox?: boolean;
5
11
  timeoutMs?: number;
6
12
  fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
7
13
  }
@@ -234,9 +240,37 @@ export class KuvarPayServer {
234
240
  renewSubscription(subscriptionId: string, body?: Record<string, any>): Promise<any>;
235
241
 
236
242
  // Banks
237
- getBanks(currency: string, options?: { type?: 'bank' | 'mobile_money'; direction?: 'inbound' | 'outbound' }): Promise<{ methods: Array<{ methodId: string; name: string; currency: string; type: 'bank' | 'mobile_money'; manualInput?: boolean; minAmount?: number | null; maxAmount?: number | null }> }>;
238
- getBanks(params: { country?: string; currency?: string; type?: 'bank' | 'mobile_money'; direction?: 'inbound' | 'outbound' }): Promise<{ methods: Array<{ methodId: string; name: string; currency: string; type: 'bank' | 'mobile_money'; manualInput?: boolean; minAmount?: number | null; maxAmount?: number | null }> }>;
239
- resolveBankAccount(data: { bank_code: string; account_number: string; country_code?: string }): Promise<any>;
243
+ getBanks(currency: string, options?: { type?: 'bank' | 'mobile_money'; direction?: 'inbound' | 'outbound'; sandbox?: boolean }): Promise<{ methods: Array<{ methodId: string; name: string; currency: string; type: 'bank' | 'mobile_money'; manualInput?: boolean; minAmount?: number | null; maxAmount?: number | null }> }>;
244
+ /**
245
+ * `sandbox` must match the mode of the business your key belongs to — live and
246
+ * sandbox are separate methodId spaces. Defaults to the client-level `sandbox`
247
+ * option. Using the wrong one yields `reason: 'METHOD_MODE_MISMATCH'` on resolve.
248
+ */
249
+ getBanks(params: { country?: string; currency?: string; type?: 'bank' | 'mobile_money'; direction?: 'inbound' | 'outbound'; sandbox?: boolean }): Promise<{ methods: Array<{ methodId: string; name: string; currency: string; type: 'bank' | 'mobile_money'; manualInput?: boolean; minAmount?: number | null; maxAmount?: number | null }> }>;
250
+ /**
251
+ * @deprecated Use `resolveRecipient`. Thin alias over
252
+ * `POST /api/v1/fiat/resolve-recipient`.
253
+ *
254
+ * The legacy `/api/v1/banks/resolve` endpoint was retired; passing `bank_code`
255
+ * now throws. Supply `methodId` from `getBanks({ direction: 'outbound' })`.
256
+ */
257
+ resolveBankAccount(data: {
258
+ methodId?: string;
259
+ method_id?: string;
260
+ accountNumber?: string;
261
+ account_number?: string;
262
+ phone?: string;
263
+ }): Promise<{
264
+ supported: boolean;
265
+ resolved: boolean;
266
+ accountName: string | null;
267
+ bankName?: string;
268
+ reason?: string | null;
269
+ /** Legacy alias for `accountName`. */
270
+ account_name: string | null;
271
+ /** Echo of the submitted account number. */
272
+ account_number: string | null;
273
+ }>;
240
274
 
241
275
  // Fiat collection (Pay with Local Currency)
242
276
  /**
@@ -253,6 +287,7 @@ export class KuvarPayServer {
253
287
  * which is a CURRENCY — pass it as `currency`, or as `country` and the SDK
254
288
  * will route 3-letter codes correctly.
255
289
  */
290
+ /** @deprecated Alias for `getBanks`, which it delegates to. */
256
291
  getFiatMethods(params: { country?: string; currency?: string; type?: 'bank' | 'mobile_money'; sandbox?: boolean; direction?: 'inbound' | 'outbound' }): Promise<{ methods: Array<{ methodId: string; name: string; currency: string; type: 'bank' | 'mobile_money'; manualInput?: boolean; minAmount?: number | null; maxAmount?: number | null }> }>;
257
292
  getFiatCheckoutQuote(params: { amount: number; baseCurrency: string; currency: string; country: string }): Promise<any>;
258
293
  createFiatTransaction(data: {
package/index.js CHANGED
@@ -64,10 +64,16 @@ class KuvarPayServer {
64
64
  * @param {number} [config.timeoutMs] - Request timeout in ms (default 60000)
65
65
  * @param {Function} [config.fetchImpl] - Optional fetch implementation for Node < 18
66
66
  */
67
- constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl } = {}) {
67
+ constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl, sandbox } = {}) {
68
68
  this.baseUrl = (baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
69
69
  this.businessId = businessId || process.env.KUVARPAY_BUSINESS_ID;
70
70
  this.secretApiKey = secretApiKey || process.env.KUVARPAY_SECRET_API_KEY || process.env.PAYMENT_SECRET_API_KEY;
71
+ // Set this to true when the key belongs to a sandbox-mode business, so
72
+ // getBanks() returns sandbox methodIds. Mixing the two spaces produces
73
+ // METHOD_MODE_MISMATCH on resolve and payout.
74
+ this.sandbox = sandbox != null
75
+ ? !!sandbox
76
+ : String(process.env.KUVARPAY_SANDBOX || '').toLowerCase() === 'true';
71
77
  this.timeoutMs = timeoutMs || 60000;
72
78
  this.fetchImpl = fetchImpl || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
73
79
 
@@ -540,21 +546,62 @@ class KuvarPayServer {
540
546
  // (settlement) methods.
541
547
  const { country, currency, type, direction = 'inbound' } = params || {};
542
548
  if (!country && !currency) throw new Error('country or currency is required');
549
+ // Live and sandbox are SEPARATE methodId spaces — the same bank has a
550
+ // different id in each. A methodId from the wrong space is rejected
551
+ // downstream with reason METHOD_MODE_MISMATCH, so the list must be fetched
552
+ // in the same mode the key's business is in. Defaults to the client-level
553
+ // `sandbox` flag when the call doesn't override it.
554
+ const sandbox = params.sandbox != null ? params.sandbox : this.sandbox;
555
+
543
556
  const qs = new URLSearchParams();
544
- if (country) qs.set('country', country);
545
- if (currency) qs.set('currency', currency);
557
+ if (currency) qs.set('currency', String(currency).toUpperCase());
558
+ if (country) {
559
+ // A country and a currency go in DIFFERENT query params, and
560
+ // getFiatCountries() returns its identifier as `code` — which is a
561
+ // CURRENCY. Passing that straight through as `country=KES` silently
562
+ // returns { methods: [] }. 2 chars -> country, 3 chars -> currency.
563
+ const code = String(country).toUpperCase();
564
+ if (!currency && code.length === 3) qs.set('currency', code);
565
+ else qs.set('country', code);
566
+ }
546
567
  if (type) qs.set('type', type);
547
568
  if (direction) qs.set('direction', direction);
569
+ if (sandbox) qs.set('sandbox', 'true');
548
570
  return this._get(`/api/v1/fiat/banks?${qs.toString()}`);
549
571
  }
550
572
 
551
573
  /**
552
- * Resolve bank account name
553
- * POST /api/v1/banks/resolve
554
- * @param {Object} data - { bank_code, account_number, country_code? }
574
+ * Resolve a bank / mobile-money account name.
575
+ *
576
+ * @deprecated Use {@link resolveRecipient}. Kept as a thin alias so existing
577
+ * integrations keep working.
578
+ *
579
+ * The legacy `POST /api/v1/banks/resolve` endpoint (which took a raw
580
+ * `bank_code`) has been retired — calling it now 404s. Name enquiry runs
581
+ * through `POST /api/v1/fiat/resolve-recipient`, which is keyed off the opaque
582
+ * `methodId` from `getBanks()`.
583
+ *
584
+ * @param {Object} data - { methodId | method_id, accountNumber | account_number, phone }
555
585
  */
556
- async resolveBankAccount(data) {
557
- return this._post('/api/v1/banks/resolve', data);
586
+ async resolveBankAccount(data = {}) {
587
+ const methodId = data.methodId || data.method_id;
588
+ const accountNumber = data.accountNumber || data.account_number;
589
+ const phone = data.phone;
590
+
591
+ if (!methodId) {
592
+ throw new Error(
593
+ (data.bank_code || data.bankCode)
594
+ ? 'resolveBankAccount no longer accepts bank_code — the legacy /api/v1/banks endpoints were retired. ' +
595
+ 'Pass methodId from getBanks({ country, direction: "outbound" }) instead, or call resolveRecipient() directly.'
596
+ : 'methodId is required (get one from getBanks({ country, direction: "outbound" }))'
597
+ );
598
+ }
599
+
600
+ const result = await this.resolveRecipient({ methodId, accountNumber, phone });
601
+ return Object.assign({}, result, {
602
+ account_name: result && result.accountName != null ? result.accountName : null,
603
+ account_number: accountNumber != null ? accountNumber : null,
604
+ });
558
605
  }
559
606
 
560
607
  // --- Fiat collection (Pay with Local Currency) ---
@@ -581,21 +628,14 @@ class KuvarPayServer {
581
628
  *
582
629
  * @param {Object} params - { country?, currency?, type?, sandbox?, direction? }
583
630
  */
631
+ /**
632
+ * @deprecated Use `getBanks`, which this now delegates to. Both hit
633
+ * `GET /api/v1/fiat/banks`; having two names for one endpoint is how the
634
+ * missing `sandbox` support on `getBanks` went unnoticed. Kept as an alias so
635
+ * existing integrations keep working.
636
+ */
584
637
  async getFiatMethods(params = {}) {
585
- const { country, currency, type, sandbox, direction = 'inbound' } = params;
586
- if (!country && !currency) throw new Error('country or currency is required');
587
-
588
- const qs = new URLSearchParams({ direction });
589
- if (currency) qs.set('currency', String(currency).toUpperCase());
590
- if (country) {
591
- const code = String(country).toUpperCase();
592
- // A 3-letter code is a currency, not a country.
593
- if (!currency && code.length === 3) qs.set('currency', code);
594
- else qs.set('country', code);
595
- }
596
- if (type) qs.set('type', type);
597
- if (sandbox) qs.set('sandbox', 'true');
598
- return this._get(`/api/v1/fiat/banks?${qs.toString()}`);
638
+ return this.getBanks(params);
599
639
  }
600
640
 
601
641
  /**
package/index.mjs CHANGED
@@ -61,10 +61,16 @@ export class KuvarPayServer {
61
61
  * @param {number} [config.timeoutMs] - Request timeout in ms (default 60000)
62
62
  * @param {Function} [config.fetchImpl] - Optional fetch implementation for Node < 18
63
63
  */
64
- constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl } = {}) {
64
+ constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl, sandbox } = {}) {
65
65
  this.baseUrl = (baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
66
66
  this.businessId = businessId || process.env.KUVARPAY_BUSINESS_ID;
67
67
  this.secretApiKey = secretApiKey || process.env.KUVARPAY_SECRET_API_KEY || process.env.PAYMENT_SECRET_API_KEY;
68
+ // Set this to true when the key belongs to a sandbox-mode business, so
69
+ // getBanks() returns sandbox methodIds. Mixing the two spaces produces
70
+ // METHOD_MODE_MISMATCH on resolve and payout.
71
+ this.sandbox = sandbox != null
72
+ ? !!sandbox
73
+ : String(process.env.KUVARPAY_SANDBOX || '').toLowerCase() === 'true';
68
74
  this.timeoutMs = timeoutMs || 60000;
69
75
  this.fetchImpl = fetchImpl || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
70
76
 
@@ -428,16 +434,64 @@ export class KuvarPayServer {
428
434
  // (settlement) methods.
429
435
  const { country, currency, type, direction = 'inbound' } = params || {};
430
436
  if (!country && !currency) throw new Error('country or currency is required');
437
+ // Live and sandbox are SEPARATE methodId spaces — the same bank has a
438
+ // different id in each. A methodId from the wrong space is rejected
439
+ // downstream with reason METHOD_MODE_MISMATCH, so the list must be fetched
440
+ // in the same mode the key's business is in. Defaults to the client-level
441
+ // `sandbox` flag when the call doesn't override it.
442
+ const sandbox = params.sandbox != null ? params.sandbox : this.sandbox;
443
+
431
444
  const qs = new URLSearchParams();
432
- if (country) qs.set('country', country);
433
- if (currency) qs.set('currency', currency);
445
+ if (currency) qs.set('currency', String(currency).toUpperCase());
446
+ if (country) {
447
+ // A country and a currency go in DIFFERENT query params, and
448
+ // getFiatCountries() returns its identifier as `code` — which is a
449
+ // CURRENCY. Passing that straight through as `country=KES` silently
450
+ // returns { methods: [] }. 2 chars → country, 3 chars → currency.
451
+ const code = String(country).toUpperCase();
452
+ if (!currency && code.length === 3) qs.set('currency', code);
453
+ else qs.set('country', code);
454
+ }
434
455
  if (type) qs.set('type', type);
435
456
  if (direction) qs.set('direction', direction);
457
+ if (sandbox) qs.set('sandbox', 'true');
436
458
  return this._get(`/api/v1/fiat/banks?${qs.toString()}`);
437
459
  }
438
460
 
439
- async resolveBankAccount(data) {
440
- return this._post('/api/v1/banks/resolve', data);
461
+ /**
462
+ * @deprecated Use {@link resolveRecipient}. Kept as a thin alias so existing
463
+ * integrations keep working.
464
+ *
465
+ * The legacy `POST /api/v1/banks/resolve` endpoint (which took a raw
466
+ * `bank_code`) has been retired — calling it now 404s. Name enquiry runs
467
+ * through `POST /api/v1/fiat/resolve-recipient`, which is keyed off the opaque
468
+ * `methodId` from `getBanks()`.
469
+ *
470
+ * Accepts either the new field names or the legacy snake_case ones:
471
+ * { methodId | method_id, accountNumber | account_number, phone }
472
+ *
473
+ * Returns the resolveRecipient shape ({ supported, resolved, accountName, … })
474
+ * plus `account_name` / `account_number` aliases for older call sites.
475
+ */
476
+ async resolveBankAccount(data = {}) {
477
+ const methodId = data.methodId || data.method_id;
478
+ const accountNumber = data.accountNumber || data.account_number;
479
+ const phone = data.phone;
480
+
481
+ if (!methodId) {
482
+ throw new Error(
483
+ (data.bank_code || data.bankCode)
484
+ ? 'resolveBankAccount no longer accepts bank_code — the legacy /api/v1/banks endpoints were retired. ' +
485
+ 'Pass methodId from getBanks({ country, direction: "outbound" }) instead, or call resolveRecipient() directly.'
486
+ : 'methodId is required (get one from getBanks({ country, direction: "outbound" }))'
487
+ );
488
+ }
489
+
490
+ const result = await this.resolveRecipient({ methodId, accountNumber, phone });
491
+ return Object.assign({}, result, {
492
+ account_name: result && result.accountName != null ? result.accountName : null,
493
+ account_number: accountNumber != null ? accountNumber : null,
494
+ });
441
495
  }
442
496
 
443
497
  // --- Fiat collection (Pay with Local Currency) ---
@@ -448,28 +502,14 @@ export class KuvarPayServer {
448
502
  return normalizeFiatCountries(data);
449
503
  }
450
504
 
451
- /** Payment methods (banks / MoMo operators) for a country. */
452
505
  /**
453
- * Payment methods for a corridor. Accepts an ISO-3166 alpha-2 country ("KE")
454
- * or an ISO-4217 currency ("KES") — the gateway resolves either, but they go
455
- * in DIFFERENT query params. `getFiatCountries()` returns its identifier as
456
- * `code`, which is a CURRENCY, so passing it through as `country=KES`
457
- * silently returned `{ methods: [] }`. 2 chars → country, 3 chars → currency.
506
+ * @deprecated Use {@link getBanks}, which this now delegates to. Both hit
507
+ * `GET /api/v1/fiat/banks`; having two names for one endpoint is how the
508
+ * missing `sandbox` support on `getBanks` went unnoticed. Kept as an alias so
509
+ * existing integrations keep working.
458
510
  */
459
511
  async getFiatMethods(params = {}) {
460
- const { country, currency, type, sandbox, direction = 'inbound' } = params;
461
- if (!country && !currency) throw new Error('country or currency is required');
462
-
463
- const qs = new URLSearchParams({ direction });
464
- if (currency) qs.set('currency', String(currency).toUpperCase());
465
- if (country) {
466
- const code = String(country).toUpperCase();
467
- if (!currency && code.length === 3) qs.set('currency', code);
468
- else qs.set('country', code);
469
- }
470
- if (type) qs.set('type', type);
471
- if (sandbox) qs.set('sandbox', 'true');
472
- return this._get(`/api/v1/fiat/banks?${qs.toString()}`);
512
+ return this.getBanks(params);
473
513
  }
474
514
 
475
515
  /** Preview the local amount a customer must pay to settle a checkout amount. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kuvarpay/sdk",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Official KuvarPay Server SDK for Node.js",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -30,4 +30,4 @@
30
30
  },
31
31
  "author": "KuvarPay Team",
32
32
  "license": "MIT"
33
- }
33
+ }