@kuvarpay/sdk 1.0.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 +162 -0
- package/index.d.ts +134 -0
- package/index.js +494 -0
- package/index.mjs +344 -0
- package/package.json +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# KuvarPay Server SDK
|
|
2
|
+
|
|
3
|
+
Backend-focused SDK for creating and verifying checkout sessions, subscription sessions, invoices, and transactions using your Business SECRET API key.
|
|
4
|
+
|
|
5
|
+
Works with Node 18+ (global `fetch`) or any environment where you can supply a `fetch` implementation. Also includes quick PHP cURL examples.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
This repository includes the SDK source files. You can copy `server-sdk/index.js` (CommonJS) or `server-sdk/index.mjs` (ESM) into your backend project, or import them via your build tooling.
|
|
10
|
+
|
|
11
|
+
> Requirements:
|
|
12
|
+
> - Business ID
|
|
13
|
+
> - SECRET API Key (server-only)
|
|
14
|
+
> - Payment API Base URL (e.g., `https://pay.kuvarpay.com` or your payment host)
|
|
15
|
+
|
|
16
|
+
## Node (CommonJS) Usage
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
// index.js (Node 18+)
|
|
20
|
+
const { KuvarPayServer } = require('./server-sdk/index.js');
|
|
21
|
+
|
|
22
|
+
const kv = new KuvarPayServer({
|
|
23
|
+
baseUrl: process.env.PAYMENT_API_BASE_URL, // e.g., https://pay.kuvarpay.com
|
|
24
|
+
businessId: process.env.KUVARPAY_BUSINESS_ID,
|
|
25
|
+
secretApiKey: process.env.KUVARPAY_SECRET_API_KEY,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function verify(sessionId) {
|
|
29
|
+
const result = await kv.verifyPayment(sessionId);
|
|
30
|
+
console.log('Verified?', result.verified, 'status:', result.status);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function session(sessionId) {
|
|
34
|
+
const s = await kv.getSessionStatus(sessionId);
|
|
35
|
+
console.log('Session:', s);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function txn(transactionId) {
|
|
39
|
+
const t = await kv.getTransactionStatus(transactionId);
|
|
40
|
+
console.log('Transaction:', t);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Node (ESM) Usage
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// index.mjs (Node 18+)
|
|
48
|
+
import { KuvarPayServer } from './server-sdk/index.mjs';
|
|
49
|
+
|
|
50
|
+
const kv = new KuvarPayServer({
|
|
51
|
+
baseUrl: process.env.PAYMENT_API_BASE_URL,
|
|
52
|
+
businessId: process.env.KUVARPAY_BUSINESS_ID,
|
|
53
|
+
secretApiKey: process.env.KUVARPAY_SECRET_API_KEY,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await kv.verifyPayment('sess_123');
|
|
57
|
+
console.log(result);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Node < 18
|
|
61
|
+
|
|
62
|
+
Provide your own `fetchImpl` (e.g., from `node-fetch`) when constructing:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const fetch = (...args) => import('node-fetch').then(mod => mod.default(...args));
|
|
66
|
+
const kv = new KuvarPayServer({ baseUrl, businessId, secretApiKey, fetchImpl: fetch });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## PHP cURL Examples
|
|
70
|
+
|
|
71
|
+
Replace `PAYMENT_API_BASE_URL`, `BUSINESS_ID`, and `SECRET_API_KEY` with your values.
|
|
72
|
+
|
|
73
|
+
### Verify Payment (Checkout Session)
|
|
74
|
+
|
|
75
|
+
```php
|
|
76
|
+
$base = getenv('PAYMENT_API_BASE_URL');
|
|
77
|
+
$businessId = getenv('KUVARPAY_BUSINESS_ID');
|
|
78
|
+
$secret = getenv('KUVARPAY_SECRET_API_KEY');
|
|
79
|
+
$sessionId = 'sess_123';
|
|
80
|
+
|
|
81
|
+
$ch = curl_init("$base/api/v1/checkout-sessions/" . urlencode($sessionId));
|
|
82
|
+
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
83
|
+
"Accept: application/json",
|
|
84
|
+
"X-API-Key: $secret",
|
|
85
|
+
"X-Business-ID: $businessId"
|
|
86
|
+
]);
|
|
87
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
88
|
+
$resp = curl_exec($ch);
|
|
89
|
+
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
90
|
+
curl_close($ch);
|
|
91
|
+
|
|
92
|
+
if ($statusCode >= 200 && $statusCode < 300) {
|
|
93
|
+
$data = json_decode($resp, true);
|
|
94
|
+
$status = $data['status'] ?? ($data['data']['status'] ?? null);
|
|
95
|
+
$verified = ($status === 'COMPLETED');
|
|
96
|
+
// handle $verified
|
|
97
|
+
} else {
|
|
98
|
+
// handle error
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Transaction Status
|
|
103
|
+
|
|
104
|
+
```php
|
|
105
|
+
$transactionId = 'tx_123';
|
|
106
|
+
$ch = curl_init("$base/api/v1/transactions/" . urlencode($transactionId));
|
|
107
|
+
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
|
108
|
+
"Accept: application/json",
|
|
109
|
+
"X-API-Key: $secret",
|
|
110
|
+
"X-Business-ID: $businessId"
|
|
111
|
+
]);
|
|
112
|
+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
113
|
+
$resp = curl_exec($ch);
|
|
114
|
+
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
115
|
+
curl_close($ch);
|
|
116
|
+
|
|
117
|
+
if ($statusCode >= 200 && $statusCode < 300) {
|
|
118
|
+
$data = json_decode($resp, true);
|
|
119
|
+
$status = $data['status'] ?? null;
|
|
120
|
+
// handle $status
|
|
121
|
+
} else {
|
|
122
|
+
// handle error
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## API Reference
|
|
127
|
+
|
|
128
|
+
Payments:
|
|
129
|
+
- `createCheckoutSession(data)` → `{ sessionId, authToken?, approvalUrl?, raw }`
|
|
130
|
+
- `getSessionStatus(sessionId)` → `{ sessionId, status, transactionId?, raw }`
|
|
131
|
+
- `getTransactionStatus(transactionId)` → `{ transactionId, status, raw }`
|
|
132
|
+
- `verifyPayment(sessionId)` → `{ verified: boolean, status, sessionId, transactionId?, raw }`
|
|
133
|
+
|
|
134
|
+
Subscriptions:
|
|
135
|
+
- `createSubscriptionCheckoutSession(data)` → `{ sessionId, approvalUrl?, raw }`
|
|
136
|
+
- `getSubscriptionCheckoutSession(id)` → full session details
|
|
137
|
+
- `confirmSubscriptionCheckoutSession(id, body?)` → confirmation response
|
|
138
|
+
- `getSubscription(subscriptionId)` → full subscription details
|
|
139
|
+
- `cancelSubscription(subscriptionId, body?)` → cancellation response
|
|
140
|
+
- `createMeteredInvoice(subscriptionId, invoiceData)` → invoice response
|
|
141
|
+
- `createSubscriptionInvoice(subscriptionId, invoiceData)` → alias to metered invoice
|
|
142
|
+
- `scheduleSubscriptionInvoice(subscriptionId, invoiceData)` → enforces `chargeSchedule: 'SCHEDULED'`
|
|
143
|
+
- `createRenewalCheckoutSession(body)` → renewal checkout creation
|
|
144
|
+
- `renewSubscription(subscriptionId, body?)` → direct renew trigger
|
|
145
|
+
|
|
146
|
+
Relay Authorization (metered):
|
|
147
|
+
- `createRelayAuthorization(body)`
|
|
148
|
+
- `getRelayAuthorizationStatus(body)`
|
|
149
|
+
- `revokeRelayAuthorization(body)` (supports legacy path via `{ useLegacyPath: true }`)
|
|
150
|
+
|
|
151
|
+
Notes:
|
|
152
|
+
- Status values commonly include `PENDING`, `ARMED`, `PROCESSING`, `COMPLETED`, `FAILED`, `EXPIRED`, `CANCELLED`, etc.
|
|
153
|
+
- `verifyPayment` returns `verified = true` when `status === 'COMPLETED'`.
|
|
154
|
+
|
|
155
|
+
## Security
|
|
156
|
+
|
|
157
|
+
- Always keep your SECRET API key in server-side environment variables. Never expose it to the browser.
|
|
158
|
+
- Use HTTPS for all API calls.
|
|
159
|
+
|
|
160
|
+
## Parity with Client SDK
|
|
161
|
+
|
|
162
|
+
The Server SDK provides server-side equivalents of the Client SDK’s network operations (creating sessions, fetching statuses, managing subscriptions, and invoices). It does not implement browser UI features like `openPayment(...)` / `openSubscription(...)` modals, postMessage callbacks, or iframe overlays. Use the Client SDK in the browser for UI, and the Server SDK on your backend for secure API operations.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export interface KuvarPayServerConfig {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
businessId?: string;
|
|
4
|
+
secretApiKey?: string;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
fetchImpl?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface SessionStatusResult {
|
|
10
|
+
sessionId: string;
|
|
11
|
+
status?: string;
|
|
12
|
+
transactionId: string | null;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
raw: any;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TransactionStatusResult {
|
|
18
|
+
transactionId: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
metadata?: Record<string, any>;
|
|
21
|
+
raw: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface VerifyPaymentResult {
|
|
25
|
+
verified: boolean;
|
|
26
|
+
status?: string;
|
|
27
|
+
sessionId: string;
|
|
28
|
+
transactionId: string | null;
|
|
29
|
+
metadata?: Record<string, any>;
|
|
30
|
+
raw: any;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class KuvarPayServer {
|
|
34
|
+
constructor(config?: KuvarPayServerConfig);
|
|
35
|
+
getSessionStatus(sessionId: string): Promise<SessionStatusResult>;
|
|
36
|
+
getTransactionStatus(transactionId: string): Promise<TransactionStatusResult>;
|
|
37
|
+
getTransactionStatusByReference(reference: string): Promise<any>;
|
|
38
|
+
verifyPayment(sessionId: string): Promise<VerifyPaymentResult>;
|
|
39
|
+
verifyWebhookSignature(payload: string | Buffer, signature: string, secret: string): boolean;
|
|
40
|
+
|
|
41
|
+
// Payments
|
|
42
|
+
getCurrencies(params?: Record<string, any>): Promise<any>;
|
|
43
|
+
calculatePayment(data: { fromCurrency: string; fromNetwork: string; toCurrency: string; toAmount: number }): Promise<any>;
|
|
44
|
+
createCheckoutSession(checkoutSessionData: {
|
|
45
|
+
amount: number;
|
|
46
|
+
currency: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
redirectUrl?: string;
|
|
49
|
+
callbackUrl?: string;
|
|
50
|
+
customerEmail?: string;
|
|
51
|
+
customerName?: string;
|
|
52
|
+
expiresIn?: number;
|
|
53
|
+
subaccount?: string;
|
|
54
|
+
split_code?: string;
|
|
55
|
+
metadata?: Record<string, any>;
|
|
56
|
+
[key: string]: any;
|
|
57
|
+
}): Promise<{ sessionId: string; authToken?: string; approvalUrl?: string; raw: any }>;
|
|
58
|
+
|
|
59
|
+
createTransaction(transactionData: {
|
|
60
|
+
amount: number;
|
|
61
|
+
currency: string;
|
|
62
|
+
fromCurrency: string;
|
|
63
|
+
fromNetwork: string;
|
|
64
|
+
toCurrency: string;
|
|
65
|
+
subaccountCode?: string;
|
|
66
|
+
splitCode?: string;
|
|
67
|
+
description?: string;
|
|
68
|
+
callbackUrl?: string;
|
|
69
|
+
metadata?: Record<string, any>;
|
|
70
|
+
[key: string]: any;
|
|
71
|
+
}): Promise<any>;
|
|
72
|
+
|
|
73
|
+
listTransactions(params?: {
|
|
74
|
+
page?: number;
|
|
75
|
+
perPage?: number;
|
|
76
|
+
status?: string;
|
|
77
|
+
fromDate?: string;
|
|
78
|
+
toDate?: string;
|
|
79
|
+
}): Promise<any>;
|
|
80
|
+
|
|
81
|
+
refundTransaction(transactionId: string, data?: { amount?: number; reason?: string }): Promise<any>;
|
|
82
|
+
getExpiredTransactions(): Promise<any>;
|
|
83
|
+
getOptimalTransferFee(amount: number, currency?: string): Promise<any>;
|
|
84
|
+
sendInvoiceEmail(sessionId: string): Promise<any>;
|
|
85
|
+
|
|
86
|
+
// Subaccounts
|
|
87
|
+
createSubaccount(subaccountData: Record<string, any>): Promise<any>;
|
|
88
|
+
listSubaccounts(params?: { page?: number; perPage?: number }): Promise<any>;
|
|
89
|
+
getSubaccount(code: string): Promise<any>;
|
|
90
|
+
updateSubaccount(code: string, subaccountData: Record<string, any>): Promise<any>;
|
|
91
|
+
deleteSubaccount(code: string): Promise<any>;
|
|
92
|
+
|
|
93
|
+
// Split Groups
|
|
94
|
+
createSplitGroup(splitGroupData: Record<string, any>): Promise<any>;
|
|
95
|
+
listSplitGroups(): Promise<any>;
|
|
96
|
+
getSplitGroup(code: string): Promise<any>;
|
|
97
|
+
updateSplitGroup(code: string, splitGroupData: Record<string, any>): Promise<any>;
|
|
98
|
+
deleteSplitGroup(code: string): Promise<any>;
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
// Subscriptions
|
|
102
|
+
createSubscriptionCheckoutSession(subscriptionData: Record<string, any>): Promise<{ sessionId: string; approvalUrl?: string; raw: any }>;
|
|
103
|
+
getSubscriptionCheckoutSession(id: string): Promise<any>;
|
|
104
|
+
confirmSubscriptionCheckoutSession(id: string, body?: Record<string, any>): Promise<any>;
|
|
105
|
+
|
|
106
|
+
// Invoices
|
|
107
|
+
createMeteredInvoice(subscriptionId: string, invoiceData: Record<string, any>): Promise<any>;
|
|
108
|
+
createSubscriptionInvoice(subscriptionId: string, invoiceData: Record<string, any>): Promise<any>;
|
|
109
|
+
scheduleSubscriptionInvoice(subscriptionId: string, invoiceData: Record<string, any>): Promise<any>;
|
|
110
|
+
|
|
111
|
+
// Subscription details & lifecycle
|
|
112
|
+
getSubscription(subscriptionId: string): Promise<any>;
|
|
113
|
+
cancelSubscription(subscriptionId: string, body?: Record<string, any>): Promise<any>;
|
|
114
|
+
|
|
115
|
+
// Relay Authorization
|
|
116
|
+
createRelayAuthorization(body: Record<string, any>): Promise<any>;
|
|
117
|
+
getRelayAuthorizationStatus(body: Record<string, any>): Promise<any>;
|
|
118
|
+
revokeRelayAuthorization(body: Record<string, any>): Promise<any>;
|
|
119
|
+
|
|
120
|
+
// Renewal
|
|
121
|
+
createRenewalCheckoutSession(body: Record<string, any>): Promise<any>;
|
|
122
|
+
renewSubscription(subscriptionId: string, body?: Record<string, any>): Promise<any>;
|
|
123
|
+
|
|
124
|
+
// Banks
|
|
125
|
+
getBanks(params?: { currency?: string; type?: 'bank' | 'mobile_money'; country?: string }): Promise<any>;
|
|
126
|
+
resolveBankAccount(data: { bank_code: string; account_number: string; country_code?: string }): Promise<any>;
|
|
127
|
+
|
|
128
|
+
// Sandbox
|
|
129
|
+
startSandboxSimulator(): Promise<any>;
|
|
130
|
+
stopSandboxSimulator(): Promise<any>;
|
|
131
|
+
getSandboxSimulatorStatus(): Promise<any>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default KuvarPayServer;
|
package/index.js
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KuvarPay Server SDK (CommonJS)
|
|
5
|
+
*
|
|
6
|
+
* For backend/server usage only. Requires a Business ID and SECRET API key.
|
|
7
|
+
* Works with Node 18+ (global fetch). If using older Node, pass a custom fetch
|
|
8
|
+
* implementation in the constructor (e.g., node-fetch) via { fetchImpl }.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_BASE = (process.env.PAYMENT_API_BASE_URL
|
|
14
|
+
|| process.env.PAYMENT_SERVER_URL
|
|
15
|
+
|| process.env.NEXT_PUBLIC_PAYMENT_API_BASE_URL
|
|
16
|
+
|| 'http://localhost:3002').replace(/\/+$/, '');
|
|
17
|
+
|
|
18
|
+
class KuvarPayServer {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} config
|
|
21
|
+
* @param {string} [config.baseUrl] - Payment API Base URL (e.g., https://pay.kuvarpay.com or https://api.yourpaymenthost.com)
|
|
22
|
+
* @param {string} [config.businessId] - Business ID
|
|
23
|
+
* @param {string} [config.secretApiKey] - Secret API key for server authentication
|
|
24
|
+
* @param {number} [config.timeoutMs] - Request timeout in ms (default 60000)
|
|
25
|
+
* @param {Function} [config.fetchImpl] - Optional fetch implementation for Node < 18
|
|
26
|
+
*/
|
|
27
|
+
constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl } = {}) {
|
|
28
|
+
this.baseUrl = (baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
|
|
29
|
+
this.businessId = businessId || process.env.KUVARPAY_BUSINESS_ID;
|
|
30
|
+
this.secretApiKey = secretApiKey || process.env.KUVARPAY_SECRET_API_KEY || process.env.PAYMENT_SECRET_API_KEY;
|
|
31
|
+
this.timeoutMs = timeoutMs || 60000;
|
|
32
|
+
this.fetchImpl = fetchImpl || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
|
|
33
|
+
|
|
34
|
+
if (!this.secretApiKey) throw new Error('secretApiKey is required');
|
|
35
|
+
if (!this.businessId) throw new Error('businessId is required');
|
|
36
|
+
if (!this.fetchImpl) throw new Error('A fetch implementation is required (Node 18+ or provide fetchImpl)');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_headers(extra = {}) {
|
|
40
|
+
return Object.assign({
|
|
41
|
+
'Accept': 'application/json',
|
|
42
|
+
'Authorization': this.secretApiKey,
|
|
43
|
+
'X-API-Key': this.secretApiKey,
|
|
44
|
+
'X-Business-ID': this.businessId,
|
|
45
|
+
}, extra);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_buildUrl(path) {
|
|
49
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
50
|
+
return `${this.baseUrl}${normalizedPath}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async _request(method, path, body, extraHeaders = {}) {
|
|
54
|
+
const url = this._buildUrl(path);
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
57
|
+
try {
|
|
58
|
+
const res = await this.fetchImpl(url, {
|
|
59
|
+
method,
|
|
60
|
+
headers: this._headers(Object.assign({
|
|
61
|
+
'Content-Type': 'application/json',
|
|
62
|
+
}, extraHeaders)),
|
|
63
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
64
|
+
signal: controller.signal,
|
|
65
|
+
});
|
|
66
|
+
clearTimeout(timeoutId);
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
let json;
|
|
69
|
+
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
const msg = (json && (json.error || json.message)) || `HTTP ${res.status}`;
|
|
72
|
+
const err = new Error(msg);
|
|
73
|
+
err.status = res.status;
|
|
74
|
+
err.body = json || text;
|
|
75
|
+
throw err;
|
|
76
|
+
}
|
|
77
|
+
return json;
|
|
78
|
+
} catch (err) {
|
|
79
|
+
clearTimeout(timeoutId);
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async _get(path) {
|
|
85
|
+
return this._request('GET', path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async _post(path, body, extraHeaders = {}) {
|
|
89
|
+
return this._request('POST', path, body, extraHeaders);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Fetch checkout session status by sessionId
|
|
94
|
+
* GET {Payment API Base}/api/v1/checkout-sessions/{sessionId}
|
|
95
|
+
* @param {string} sessionId
|
|
96
|
+
* @returns {Promise<{ sessionId: string, status: string | undefined, transactionId: string | null, raw: any }>}
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* Signature Verification for Webhooks
|
|
100
|
+
* @param {string|Buffer} payload - Raw request body
|
|
101
|
+
* @param {string} signature - Value from X-KuvarPay-Signature header
|
|
102
|
+
* @param {string} secret - Your Webhook Secret from the dashboard
|
|
103
|
+
*/
|
|
104
|
+
verifyWebhookSignature(payload, signature, secret) {
|
|
105
|
+
if (!signature || !secret) return false;
|
|
106
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
107
|
+
const digest = hmac.update(payload).digest('hex');
|
|
108
|
+
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async getSessionStatus(sessionId) {
|
|
112
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
113
|
+
throw new Error('sessionId is required');
|
|
114
|
+
}
|
|
115
|
+
const data = await this._get(`/api/v1/checkout-sessions/${encodeURIComponent(sessionId)}`);
|
|
116
|
+
const status = data?.status || data?.checkoutSession?.status || data?.data?.status;
|
|
117
|
+
const transactionId = data?.transactionId || data?.data?.transactionId || null;
|
|
118
|
+
return {
|
|
119
|
+
sessionId: data?.id || data?.sessionId || sessionId,
|
|
120
|
+
status,
|
|
121
|
+
transactionId,
|
|
122
|
+
metadata: data?.metadata || data?.checkoutSession?.metadata || data?.data?.metadata || null,
|
|
123
|
+
raw: data,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Fetch transaction status by transactionId
|
|
129
|
+
* GET {Payment API Base}/api/v1/transactions/{transactionId}
|
|
130
|
+
* @param {string} transactionId
|
|
131
|
+
* @returns {Promise<{ transactionId: string, status: string | undefined, raw: any }>}
|
|
132
|
+
*/
|
|
133
|
+
async getTransactionStatus(transactionId) {
|
|
134
|
+
if (!transactionId || typeof transactionId !== 'string') {
|
|
135
|
+
throw new Error('transactionId is required');
|
|
136
|
+
}
|
|
137
|
+
const data = await this._get(`/api/v1/transactions/${encodeURIComponent(transactionId)}`);
|
|
138
|
+
return {
|
|
139
|
+
transactionId: data?.id || transactionId,
|
|
140
|
+
status: data?.status,
|
|
141
|
+
metadata: data?.metadata || data?.data?.metadata || null,
|
|
142
|
+
raw: data,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get transaction status by merchant reference
|
|
148
|
+
* GET /api/v1/transactions/status/{reference}
|
|
149
|
+
*/
|
|
150
|
+
async getTransactionStatusByReference(reference) {
|
|
151
|
+
if (!reference) throw new Error('reference is required');
|
|
152
|
+
return this._get(`/api/v1/transactions/status/${encodeURIComponent(reference)}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* List all transactions
|
|
157
|
+
* @param {Object} params - { page, perPage, status, fromDate, toDate }
|
|
158
|
+
*/
|
|
159
|
+
async listTransactions(params = {}) {
|
|
160
|
+
const query = new URLSearchParams(params).toString();
|
|
161
|
+
const path = query ? `/api/v1/transactions?${query}` : '/api/v1/transactions';
|
|
162
|
+
return this._get(path);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Refund a transaction
|
|
167
|
+
* @param {string} transactionId
|
|
168
|
+
* @param {Object} data - { amount?, reason? }
|
|
169
|
+
*/
|
|
170
|
+
async refundTransaction(transactionId, data = {}) {
|
|
171
|
+
return this._post(`/api/v1/transactions/${encodeURIComponent(transactionId)}/refund`, data);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get expired transactions
|
|
176
|
+
*/
|
|
177
|
+
async getExpiredTransactions() {
|
|
178
|
+
return this._get('/api/v1/transactions/expired');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Calculate fees
|
|
183
|
+
*/
|
|
184
|
+
async getOptimalTransferFee(amount, currency = 'NGN') {
|
|
185
|
+
return this._get(`/api/v1/optimal-transfer-fee?amount=${amount}¤cy=${currency}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Calculate required crypto payment amount
|
|
190
|
+
* POST /api/v1/transactions/calculate-payment
|
|
191
|
+
* @param {Object} data - { fromCurrency, fromNetwork, toCurrency, toAmount }
|
|
192
|
+
*/
|
|
193
|
+
async calculatePayment(data) {
|
|
194
|
+
return this._post('/api/v1/transactions/calculate-payment', data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Convenience wrapper to verify a payment session reached a final successful state
|
|
199
|
+
* @param {string} sessionId
|
|
200
|
+
* @returns {Promise<{ verified: boolean, status: string | undefined, sessionId: string, transactionId: string | null, raw: any }>}
|
|
201
|
+
*/
|
|
202
|
+
async verifyPayment(sessionId) {
|
|
203
|
+
const s = await this.getSessionStatus(sessionId);
|
|
204
|
+
const verified = s.status === 'COMPLETED';
|
|
205
|
+
return {
|
|
206
|
+
verified,
|
|
207
|
+
status: s.status,
|
|
208
|
+
sessionId: s.sessionId,
|
|
209
|
+
transactionId: s.transactionId,
|
|
210
|
+
metadata: s.metadata,
|
|
211
|
+
raw: s.raw,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Create a one-time checkout session
|
|
217
|
+
* POST {Payment API Base}/api/v1/checkout-sessions
|
|
218
|
+
* @param {Object} checkoutSessionData - { amount, currency, description?, redirectUrl?, callbackUrl?, customerEmail?, customerName?, expiresIn?, metadata? }
|
|
219
|
+
* @returns {Promise<{ sessionId: string, authToken?: string, approvalUrl?: string, raw: any }>} // approvalUrl may be present when using redirect flows
|
|
220
|
+
*/
|
|
221
|
+
async createCheckoutSession(checkoutSessionData) {
|
|
222
|
+
const payload = Object.assign({}, checkoutSessionData, {
|
|
223
|
+
businessId: checkoutSessionData.businessId || this.businessId,
|
|
224
|
+
});
|
|
225
|
+
// payload can include 'subaccount' (string) or 'split_code' (string)
|
|
226
|
+
// Guide says /api/v1/checkout-sessions
|
|
227
|
+
const data = await this._post('/api/v1/checkout-sessions', payload);
|
|
228
|
+
return {
|
|
229
|
+
sessionId: data?.sessionId || data?.id || data?.checkoutSession?.id || data?.data?.sessionId,
|
|
230
|
+
authToken: data?.authToken || data?.data?.authToken,
|
|
231
|
+
approvalUrl: data?.approvalUrl || data?.approval_url || data?.checkoutSession?.approval_url || data?.data?.approval_url,
|
|
232
|
+
raw: data,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Create a manual transaction (SDK / Server-to-Server)
|
|
238
|
+
* POST {Payment API Base}/api/v1/transactions/create
|
|
239
|
+
* @param {Object} transactionData - { amount, currency, fromCurrency, fromNetwork, toCurrency, subaccountCode?, splitCode?, description?, callbackUrl?, metadata? }
|
|
240
|
+
* @returns {Promise<any>}
|
|
241
|
+
*/
|
|
242
|
+
async getCurrencies(params = {}) {
|
|
243
|
+
const query = new URLSearchParams(params).toString();
|
|
244
|
+
const path = query ? `/api/v1/currencies?${query}` : '/api/v1/currencies';
|
|
245
|
+
return this._get(path);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async createTransaction(transactionData) {
|
|
249
|
+
const payload = Object.assign({}, transactionData, {
|
|
250
|
+
businessId: transactionData.businessId || this.businessId,
|
|
251
|
+
});
|
|
252
|
+
return this._post('/api/v1/transactions', payload);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Email an invoice to a customer
|
|
257
|
+
*/
|
|
258
|
+
async sendInvoiceEmail(sessionId) {
|
|
259
|
+
return this._post(`/api/v1/invoices/${encodeURIComponent(sessionId)}/send-email`, {});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// --- Subaccount Management ---
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Create a new subaccount
|
|
266
|
+
* @param {Object} subaccountData - { business_name, percentage_charge, ... }
|
|
267
|
+
*/
|
|
268
|
+
async createSubaccount(subaccountData) {
|
|
269
|
+
return this._post('/api/v1/subaccounts', subaccountData);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* List all subaccounts
|
|
274
|
+
* @param {Object} [params] - { page, perPage }
|
|
275
|
+
*/
|
|
276
|
+
async listSubaccounts(params = {}) {
|
|
277
|
+
const query = new URLSearchParams(params).toString();
|
|
278
|
+
const path = query ? `/api/v1/subaccounts?${query}` : '/api/v1/subaccounts';
|
|
279
|
+
return this._get(path);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Get subaccount details
|
|
284
|
+
* @param {string} code - SUB_xxx
|
|
285
|
+
*/
|
|
286
|
+
async getSubaccount(code) {
|
|
287
|
+
return this._get(`/api/v1/subaccounts/${encodeURIComponent(code)}`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Update subaccount
|
|
292
|
+
*/
|
|
293
|
+
async updateSubaccount(code, subaccountData) {
|
|
294
|
+
return this._request('PATCH', `/api/v1/subaccounts/${encodeURIComponent(code)}`, subaccountData);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Delete subaccount (soft delete)
|
|
299
|
+
*/
|
|
300
|
+
async deleteSubaccount(code) {
|
|
301
|
+
return this._request('DELETE', `/api/v1/subaccounts/${encodeURIComponent(code)}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --- Split Group Management ---
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Create a new split group
|
|
308
|
+
* @param {Object} splitGroupData - { name, bearer_type, subaccounts: [{ subaccount, share }] }
|
|
309
|
+
*/
|
|
310
|
+
async createSplitGroup(splitGroupData) {
|
|
311
|
+
return this._post('/api/v1/split', splitGroupData);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* List all split groups
|
|
316
|
+
*/
|
|
317
|
+
async listSplitGroups() {
|
|
318
|
+
return this._get('/api/v1/split');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get split group details
|
|
323
|
+
* @param {string} code - SPL_xxx
|
|
324
|
+
*/
|
|
325
|
+
async getSplitGroup(code) {
|
|
326
|
+
return this._get(`/api/v1/split/${encodeURIComponent(code)}`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Update split group
|
|
331
|
+
*/
|
|
332
|
+
async updateSplitGroup(code, splitGroupData) {
|
|
333
|
+
return this._request('PATCH', `/api/v1/split/${encodeURIComponent(code)}`, splitGroupData);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Delete split group
|
|
338
|
+
*/
|
|
339
|
+
async deleteSplitGroup(code) {
|
|
340
|
+
return this._request('DELETE', `/api/v1/split/${encodeURIComponent(code)}`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Create a subscription checkout session (Metered or Fixed, depending on your Payment API config)
|
|
346
|
+
* POST {Payment API Base}/api/v1/subscriptions/checkout-sessions
|
|
347
|
+
* @param {Object} subscriptionData - { billingMode?, customer?, expectedUsage?, strategy?, customMultiplier?, metadata?, businessId? }
|
|
348
|
+
* @returns {Promise<{ sessionId: string, approvalUrl?: string, raw: any }>}
|
|
349
|
+
*/
|
|
350
|
+
async createSubscriptionCheckoutSession(subscriptionData) {
|
|
351
|
+
const payload = Object.assign({
|
|
352
|
+
billingMode: subscriptionData?.billingMode || 'METERED',
|
|
353
|
+
businessId: subscriptionData?.businessId || this.businessId,
|
|
354
|
+
}, subscriptionData);
|
|
355
|
+
const data = await this._post('/api/v1/subscriptions/checkout-sessions', payload);
|
|
356
|
+
return {
|
|
357
|
+
sessionId: data?.sessionId || data?.id || data?.checkoutSession?.id || data?.checkoutSession?.uid,
|
|
358
|
+
approvalUrl: data?.approvalUrl || data?.approval_url || data?.checkoutSession?.approval_url,
|
|
359
|
+
raw: data,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Get a subscription checkout session by id
|
|
365
|
+
* GET {Payment API Base}/api/v1/subscriptions/checkout-sessions/{id}
|
|
366
|
+
*/
|
|
367
|
+
async getSubscriptionCheckoutSession(id) {
|
|
368
|
+
return this._get(`/api/v1/subscriptions/checkout-sessions/${encodeURIComponent(id)}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Confirm a subscription checkout session
|
|
373
|
+
* POST {Payment API Base}/api/v1/subscriptions/checkout-sessions/{id}/confirm
|
|
374
|
+
*/
|
|
375
|
+
async confirmSubscriptionCheckoutSession(id, body = {}) {
|
|
376
|
+
return this._post(`/api/v1/subscriptions/checkout-sessions/${encodeURIComponent(id)}/confirm`, body);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Create a metered invoice for a subscription
|
|
381
|
+
* POST {Payment API Base}/api/v1/subscriptions/{subscriptionId}/metered-invoices
|
|
382
|
+
*/
|
|
383
|
+
async createMeteredInvoice(subscriptionId, invoiceData) {
|
|
384
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
385
|
+
const payload = Object.assign({}, invoiceData);
|
|
386
|
+
const data = await this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/metered-invoices`, payload);
|
|
387
|
+
return data;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// --- Banks & Account Verification ---
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* List supported banks
|
|
394
|
+
* GET /api/v1/banks
|
|
395
|
+
* @param {Object} params - { currency, type, country }
|
|
396
|
+
*/
|
|
397
|
+
async getBanks(params = {}) {
|
|
398
|
+
const query = new URLSearchParams(params).toString();
|
|
399
|
+
const path = query ? `/api/v1/banks?${query}` : '/api/v1/banks';
|
|
400
|
+
return this._get(path);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resolve bank account name
|
|
405
|
+
* POST /api/v1/banks/resolve
|
|
406
|
+
* @param {Object} data - { bank_code, account_number, country_code? }
|
|
407
|
+
*/
|
|
408
|
+
async resolveBankAccount(data) {
|
|
409
|
+
return this._post('/api/v1/banks/resolve', data);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// --- Sandbox Simulator ---
|
|
413
|
+
|
|
414
|
+
async startSandboxSimulator() {
|
|
415
|
+
return this._post('/api/v1/sandbox-simulator/start', {});
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async stopSandboxSimulator() {
|
|
419
|
+
return this._post('/api/v1/sandbox-simulator/stop', {});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async getSandboxSimulatorStatus() {
|
|
423
|
+
return this._get('/api/v1/sandbox-simulator/status');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Alias for metered invoice creation
|
|
428
|
+
*/
|
|
429
|
+
async createSubscriptionInvoice(subscriptionId, invoiceData) {
|
|
430
|
+
return this.createMeteredInvoice(subscriptionId, invoiceData);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Schedule a subscription invoice (convenience wrapper)
|
|
435
|
+
*/
|
|
436
|
+
async scheduleSubscriptionInvoice(subscriptionId, invoiceData) {
|
|
437
|
+
if (!invoiceData?.dueDate) throw new Error('dueDate is required to schedule an invoice');
|
|
438
|
+
const payload = Object.assign({}, invoiceData, { chargeSchedule: 'SCHEDULED' });
|
|
439
|
+
return this.createSubscriptionInvoice(subscriptionId, payload);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Get subscription details by id
|
|
444
|
+
* GET {Payment API Base}/api/v1/subscriptions/{subscriptionId}
|
|
445
|
+
*/
|
|
446
|
+
async getSubscription(subscriptionId) {
|
|
447
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
448
|
+
return this._get(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Cancel a subscription (confirm cancellation)
|
|
453
|
+
* POST {Payment API Base}/api/v1/subscriptions/{subscriptionId}/cancel
|
|
454
|
+
*/
|
|
455
|
+
async cancelSubscription(subscriptionId, body = {}) {
|
|
456
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
457
|
+
return this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/cancel`, body);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Relay Authorization APIs (for metered subscriptions)
|
|
462
|
+
*/
|
|
463
|
+
async createRelayAuthorization(body) {
|
|
464
|
+
return this._post('/api/v1/subscriptions/relay/authorizations', body);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async getRelayAuthorizationStatus(body) {
|
|
468
|
+
return this._post('/api/v1/subscriptions/relay/authorization-status', body);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async revokeRelayAuthorization(body) {
|
|
472
|
+
// Some environments use '/relay/revokeAuth'; support both via path param
|
|
473
|
+
const path = body?.useLegacyPath ? '/api/v1/subscriptions/relay/revokeAuth' : '/api/v1/subscriptions/revoke-authorization';
|
|
474
|
+
return this._post(path, body);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Create a renewal checkout session for an existing subscription
|
|
479
|
+
* POST {Payment API Base}/api/v1/subscriptions/renewal-checkout-sessions
|
|
480
|
+
*/
|
|
481
|
+
async createRenewalCheckoutSession(body) {
|
|
482
|
+
return this._post('/api/v1/subscriptions/renewal-checkout-sessions', body);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Trigger subscription renew
|
|
487
|
+
* POST {Payment API Base}/api/v1/subscriptions/{subscriptionId}/renew
|
|
488
|
+
*/
|
|
489
|
+
async renewSubscription(subscriptionId, body = {}) {
|
|
490
|
+
return this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/renew`, body);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
module.exports = { KuvarPayServer };
|
package/index.mjs
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KuvarPay Server SDK (ESM)
|
|
3
|
+
*
|
|
4
|
+
* For backend/server usage only. Requires a Business ID and SECRET API key.
|
|
5
|
+
* Works with Node 18+ (global fetch). If using older Node, pass a custom fetch
|
|
6
|
+
* implementation in the constructor (e.g., node-fetch) via { fetchImpl }.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_BASE = (process.env.PAYMENT_API_BASE_URL
|
|
12
|
+
|| process.env.PAYMENT_SERVER_URL
|
|
13
|
+
|| process.env.NEXT_PUBLIC_PAYMENT_API_BASE_URL
|
|
14
|
+
|| 'http://localhost:3002').replace(/\/+$/, '');
|
|
15
|
+
|
|
16
|
+
export class KuvarPayServer {
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} config
|
|
19
|
+
* @param {string} [config.baseUrl] - Payment API Base URL (e.g., https://pay.kuvarpay.com or https://api.yourpaymenthost.com)
|
|
20
|
+
* @param {string} [config.businessId] - Business ID
|
|
21
|
+
* @param {string} [config.secretApiKey] - Secret API key for server authentication
|
|
22
|
+
* @param {number} [config.timeoutMs] - Request timeout in ms (default 60000)
|
|
23
|
+
* @param {Function} [config.fetchImpl] - Optional fetch implementation for Node < 18
|
|
24
|
+
*/
|
|
25
|
+
constructor({ baseUrl, businessId, secretApiKey, timeoutMs, fetchImpl } = {}) {
|
|
26
|
+
this.baseUrl = (baseUrl || DEFAULT_BASE).replace(/\/+$/, '');
|
|
27
|
+
this.businessId = businessId || process.env.KUVARPAY_BUSINESS_ID;
|
|
28
|
+
this.secretApiKey = secretApiKey || process.env.KUVARPAY_SECRET_API_KEY || process.env.PAYMENT_SECRET_API_KEY;
|
|
29
|
+
this.timeoutMs = timeoutMs || 60000;
|
|
30
|
+
this.fetchImpl = fetchImpl || (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : null);
|
|
31
|
+
|
|
32
|
+
if (!this.secretApiKey) throw new Error('secretApiKey is required');
|
|
33
|
+
if (!this.businessId) throw new Error('businessId is required');
|
|
34
|
+
if (!this.fetchImpl) throw new Error('A fetch implementation is required (Node 18+ or provide fetchImpl)');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_headers(extra = {}) {
|
|
38
|
+
return Object.assign({
|
|
39
|
+
'Accept': 'application/json',
|
|
40
|
+
'Authorization': this.secretApiKey,
|
|
41
|
+
'X-API-Key': this.secretApiKey,
|
|
42
|
+
'X-Business-ID': this.businessId,
|
|
43
|
+
}, extra);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
_buildUrl(path) {
|
|
47
|
+
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
|
48
|
+
return `${this.baseUrl}${normalizedPath}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async _request(method, path, body, extraHeaders = {}) {
|
|
52
|
+
const url = this._buildUrl(path);
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
55
|
+
try {
|
|
56
|
+
const res = await this.fetchImpl(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers: this._headers(Object.assign({
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
}, extraHeaders)),
|
|
61
|
+
body: body != null ? JSON.stringify(body) : undefined,
|
|
62
|
+
signal: controller.signal,
|
|
63
|
+
});
|
|
64
|
+
clearTimeout(timeoutId);
|
|
65
|
+
const text = await res.text();
|
|
66
|
+
let json;
|
|
67
|
+
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
const msg = (json && (json.error || json.message)) || `HTTP ${res.status}`;
|
|
70
|
+
const err = new Error(msg);
|
|
71
|
+
err.status = res.status;
|
|
72
|
+
err.body = json || text;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
return json;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
throw err;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async _get(path) {
|
|
83
|
+
return this._request('GET', path);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async _post(path, body, extraHeaders = {}) {
|
|
87
|
+
return this._request('POST', path, body, extraHeaders);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Signature Verification for Webhooks
|
|
92
|
+
* @param {string|Buffer} payload - Raw request body
|
|
93
|
+
* @param {string} signature - Value from X-KuvarPay-Signature header
|
|
94
|
+
* @param {string} secret - Your Webhook Secret from the dashboard
|
|
95
|
+
*/
|
|
96
|
+
verifyWebhookSignature(payload, signature, secret) {
|
|
97
|
+
if (!signature || !secret) return false;
|
|
98
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
99
|
+
const digest = hmac.update(payload).digest('hex');
|
|
100
|
+
return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async getSessionStatus(sessionId) {
|
|
104
|
+
if (!sessionId || typeof sessionId !== 'string') {
|
|
105
|
+
throw new Error('sessionId is required');
|
|
106
|
+
}
|
|
107
|
+
const data = await this._get(`/api/v1/checkout-sessions/${encodeURIComponent(sessionId)}`);
|
|
108
|
+
const status = data?.status || data?.checkoutSession?.status || data?.data?.status;
|
|
109
|
+
const transactionId = data?.transactionId || data?.data?.transactionId || null;
|
|
110
|
+
return {
|
|
111
|
+
sessionId: data?.id || data?.sessionId || sessionId,
|
|
112
|
+
status,
|
|
113
|
+
transactionId,
|
|
114
|
+
metadata: data?.metadata || data?.checkoutSession?.metadata || data?.data?.metadata || null,
|
|
115
|
+
raw: data,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getTransactionStatus(transactionId) {
|
|
120
|
+
if (!transactionId || typeof transactionId !== 'string') {
|
|
121
|
+
throw new Error('transactionId is required');
|
|
122
|
+
}
|
|
123
|
+
const data = await this._get(`/api/v1/transactions/${encodeURIComponent(transactionId)}`);
|
|
124
|
+
return {
|
|
125
|
+
transactionId: data?.id || transactionId,
|
|
126
|
+
status: data?.status,
|
|
127
|
+
metadata: data?.metadata || data?.data?.metadata || null,
|
|
128
|
+
raw: data,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async getTransactionStatusByReference(reference) {
|
|
133
|
+
if (!reference) throw new Error('reference is required');
|
|
134
|
+
return this._get(`/api/v1/transactions/status/${encodeURIComponent(reference)}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async listTransactions(params = {}) {
|
|
138
|
+
const query = new URLSearchParams(params).toString();
|
|
139
|
+
const path = query ? `/api/v1/transactions?${query}` : '/api/v1/transactions';
|
|
140
|
+
return this._get(path);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async refundTransaction(transactionId, data = {}) {
|
|
144
|
+
return this._post(`/api/v1/transactions/${encodeURIComponent(transactionId)}/refund`, data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getExpiredTransactions() {
|
|
148
|
+
return this._get('/api/v1/transactions/expired');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async getOptimalTransferFee(amount, currency = 'NGN') {
|
|
152
|
+
return this._get(`/api/v1/optimal-transfer-fee?amount=${amount}¤cy=${currency}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async calculatePayment(data) {
|
|
156
|
+
return this._post('/api/v1/transactions/calculate-payment', data);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async verifyPayment(sessionId) {
|
|
160
|
+
const s = await this.getSessionStatus(sessionId);
|
|
161
|
+
const verified = s.status === 'COMPLETED';
|
|
162
|
+
return {
|
|
163
|
+
verified,
|
|
164
|
+
status: s.status,
|
|
165
|
+
sessionId: s.sessionId,
|
|
166
|
+
transactionId: s.transactionId,
|
|
167
|
+
metadata: s.metadata,
|
|
168
|
+
raw: s.raw,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async createCheckoutSession(checkoutSessionData) {
|
|
173
|
+
const payload = Object.assign({}, checkoutSessionData, {
|
|
174
|
+
businessId: checkoutSessionData.businessId || this.businessId,
|
|
175
|
+
});
|
|
176
|
+
// payload can include 'subaccount' (string) or 'split_code' (string)
|
|
177
|
+
const data = await this._post('/api/v1/checkout-sessions', payload);
|
|
178
|
+
return {
|
|
179
|
+
sessionId: data?.sessionId || data?.id || data?.checkoutSession?.id || data?.data?.sessionId,
|
|
180
|
+
authToken: data?.authToken || data?.data?.authToken,
|
|
181
|
+
approvalUrl: data?.approvalUrl || data?.approval_url || data?.checkoutSession?.approval_url || data?.data?.approval_url,
|
|
182
|
+
raw: data,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async getCurrencies(params = {}) {
|
|
187
|
+
const query = new URLSearchParams(params).toString();
|
|
188
|
+
const path = query ? `/api/v1/currencies?${query}` : '/api/v1/currencies';
|
|
189
|
+
return this._get(path);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async createTransaction(transactionData) {
|
|
193
|
+
const payload = Object.assign({}, transactionData, {
|
|
194
|
+
businessId: transactionData.businessId || this.businessId,
|
|
195
|
+
});
|
|
196
|
+
return this._post('/api/v1/transactions', payload);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async sendInvoiceEmail(sessionId) {
|
|
200
|
+
return this._post(`/api/v1/invoices/${encodeURIComponent(sessionId)}/send-email`, {});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Subaccount Management ---
|
|
204
|
+
|
|
205
|
+
async createSubaccount(subaccountData) {
|
|
206
|
+
return this._post('/api/v1/subaccounts', subaccountData);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async listSubaccounts(params = {}) {
|
|
210
|
+
const query = new URLSearchParams(params).toString();
|
|
211
|
+
const path = query ? `/api/v1/subaccounts?${query}` : '/api/v1/subaccounts';
|
|
212
|
+
return this._get(path);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async getSubaccount(code) {
|
|
216
|
+
return this._get(`/api/v1/subaccounts/${encodeURIComponent(code)}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async updateSubaccount(code, subaccountData) {
|
|
220
|
+
return this._request('PATCH', `/api/v1/subaccounts/${encodeURIComponent(code)}`, subaccountData);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async deleteSubaccount(code) {
|
|
224
|
+
return this._request('DELETE', `/api/v1/subaccounts/${encodeURIComponent(code)}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// --- Split Group Management ---
|
|
228
|
+
|
|
229
|
+
async createSplitGroup(splitGroupData) {
|
|
230
|
+
return this._post('/api/v1/split', splitGroupData);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async listSplitGroups() {
|
|
234
|
+
return this._get('/api/v1/split');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async getSplitGroup(code) {
|
|
238
|
+
return this._get(`/api/v1/split/${encodeURIComponent(code)}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async updateSplitGroup(code, splitGroupData) {
|
|
242
|
+
return this._request('PATCH', `/api/v1/split/${encodeURIComponent(code)}`, splitGroupData);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async deleteSplitGroup(code) {
|
|
246
|
+
return this._request('DELETE', `/api/v1/split/${encodeURIComponent(code)}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async createSubscriptionCheckoutSession(subscriptionData) {
|
|
251
|
+
const payload = Object.assign({
|
|
252
|
+
billingMode: subscriptionData?.billingMode || 'METERED',
|
|
253
|
+
businessId: subscriptionData?.businessId || this.businessId,
|
|
254
|
+
}, subscriptionData);
|
|
255
|
+
const data = await this._post('/api/v1/subscriptions/checkout-sessions', payload);
|
|
256
|
+
return {
|
|
257
|
+
sessionId: data?.sessionId || data?.id || data?.checkoutSession?.id || data?.checkoutSession?.uid,
|
|
258
|
+
approvalUrl: data?.approvalUrl || data?.approval_url || data?.checkoutSession?.approval_url,
|
|
259
|
+
raw: data,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async getSubscriptionCheckoutSession(id) {
|
|
264
|
+
return this._get(`/api/v1/subscriptions/checkout-sessions/${encodeURIComponent(id)}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async confirmSubscriptionCheckoutSession(id, body = {}) {
|
|
268
|
+
return this._post(`/api/v1/subscriptions/checkout-sessions/${encodeURIComponent(id)}/confirm`, body);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async createMeteredInvoice(subscriptionId, invoiceData) {
|
|
272
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
273
|
+
const payload = Object.assign({}, invoiceData);
|
|
274
|
+
const data = await this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/metered-invoices`, payload);
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Banks & Account Verification ---
|
|
279
|
+
|
|
280
|
+
async getBanks(params = {}) {
|
|
281
|
+
const query = new URLSearchParams(params).toString();
|
|
282
|
+
const path = query ? `/api/v1/banks?${query}` : '/api/v1/banks';
|
|
283
|
+
return this._get(path);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async resolveBankAccount(data) {
|
|
287
|
+
return this._post('/api/v1/banks/resolve', data);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- Sandbox Simulator ---
|
|
291
|
+
|
|
292
|
+
async startSandboxSimulator() {
|
|
293
|
+
return this._post('/api/v1/sandbox-simulator/start', {});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async stopSandboxSimulator() {
|
|
297
|
+
return this._post('/api/v1/sandbox-simulator/stop', {});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async getSandboxSimulatorStatus() {
|
|
301
|
+
return this._get('/api/v1/sandbox-simulator/status');
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async createSubscriptionInvoice(subscriptionId, invoiceData) {
|
|
305
|
+
return this.createMeteredInvoice(subscriptionId, invoiceData);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async scheduleSubscriptionInvoice(subscriptionId, invoiceData) {
|
|
309
|
+
if (!invoiceData?.dueDate) throw new Error('dueDate is required to schedule an invoice');
|
|
310
|
+
const payload = Object.assign({}, invoiceData, { chargeSchedule: 'SCHEDULED' });
|
|
311
|
+
return this.createSubscriptionInvoice(subscriptionId, payload);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async getSubscription(subscriptionId) {
|
|
315
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
316
|
+
return this._get(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async cancelSubscription(subscriptionId, body = {}) {
|
|
320
|
+
if (!subscriptionId) throw new Error('subscriptionId is required');
|
|
321
|
+
return this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/cancel`, body);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async createRelayAuthorization(body) {
|
|
325
|
+
return this._post('/api/v1/subscriptions/relay/authorizations', body);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async getRelayAuthorizationStatus(body) {
|
|
329
|
+
return this._post('/api/v1/subscriptions/relay/authorization-status', body);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async revokeRelayAuthorization(body) {
|
|
333
|
+
const path = body?.useLegacyPath ? '/api/v1/subscriptions/relay/revokeAuth' : '/api/v1/subscriptions/revoke-authorization';
|
|
334
|
+
return this._post(path, body);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async createRenewalCheckoutSession(body) {
|
|
338
|
+
return this._post('/api/v1/subscriptions/renewal-checkout-sessions', body);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async renewSubscription(subscriptionId, body = {}) {
|
|
342
|
+
return this._post(`/api/v1/subscriptions/${encodeURIComponent(subscriptionId)}/renew`, body);
|
|
343
|
+
}
|
|
344
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kuvarpay/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official KuvarPay Server SDK for Node.js",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"module": "index.mjs",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"index.d.ts",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"payments",
|
|
19
|
+
"crypto",
|
|
20
|
+
"kuvarpay",
|
|
21
|
+
"blockchain",
|
|
22
|
+
"fiat-payout",
|
|
23
|
+
"split-payments"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"author": "KuvarPay Team",
|
|
29
|
+
"license": "MIT"
|
|
30
|
+
}
|