@patalink/node-api-client 1.0.5 → 1.1.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 +149 -525
- package/dist/index.d.mts +52 -11
- package/dist/index.d.ts +52 -11
- package/dist/index.js +69 -26
- package/dist/index.mjs +59 -16
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,623 +1,247 @@
|
|
|
1
|
-
|
|
1
|
+
# @patalink/node-api-client
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@patalink/node-api-client)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
[](https://badge.fury.io/js/@patlink%2Fapi-client)
|
|
9
|
-
[](https://opensource.org/licenses/MIT)
|
|
10
|
-
[](https://nodejs.org/)
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 📖 Table of Contents
|
|
15
|
-
|
|
16
|
-
- [Quick Start](#rocket-quick-start)
|
|
17
|
-
- [Installation](#package-installation)
|
|
18
|
-
- [Configuration](#gear-configuration)
|
|
19
|
-
- [API Reference](#api-reference)
|
|
20
|
-
- [Usage Examples](#usage-examples)
|
|
21
|
-
- [Error Handling](#error-handling)
|
|
22
|
-
- [TypeScript Support](#typescript-support)
|
|
23
|
-
- [Browser Support](#browser-support)
|
|
24
|
-
- [FAQ](#faq)
|
|
8
|
+
The official server-side Node.js SDK for **[PataLink](https://patalink.me)**. Integrate Mobile Money collections, instant B2C payouts (MTN Rwanda), real-time wallet balances, and webhook processing with automatic end-to-end payload encryption (AES-256-CBC + HMAC-SHA256).
|
|
25
9
|
|
|
26
10
|
---
|
|
27
11
|
|
|
28
|
-
##
|
|
29
|
-
|
|
30
|
-
Add payment processing to your Node.js backend in 3 simple steps:
|
|
31
|
-
|
|
32
|
-
### Step 1: Install the package
|
|
33
|
-
|
|
34
|
-
```bash
|
|
35
|
-
npm install @patlink/node-api-client
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
### Step 2: Initialize the client
|
|
39
|
-
|
|
40
|
-
```javascript
|
|
41
|
-
import { createClient } from '@patlink/node-api-client';
|
|
42
|
-
|
|
43
|
-
const client = createClient({
|
|
44
|
-
apiKey: 'pk_live_abc123...',
|
|
45
|
-
encryptionKey: 'xK8j3Hd9Fq2Wm5R...',
|
|
46
|
-
baseUrl: 'https://patalink.me'
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Step 3: Create a payment
|
|
12
|
+
## ⚡ Features
|
|
51
13
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
customerEmail: 'john@example.com'
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
console.log('Transaction ID:', payment.transactionId);
|
|
62
|
-
```
|
|
14
|
+
- **Inbound Collections**: Accept customer payments via MTN Mobile Money USSD prompt and card checkout (PesaPal).
|
|
15
|
+
- **Instant B2C Disbursements**: Send payouts directly to any MTN Mobile Money number in Rwanda (`078...` / `079...`).
|
|
16
|
+
- **Live Wallet Balances**: Check available cleared funds, pending amounts, and total volume before initiating payouts.
|
|
17
|
+
- **Automated Encryption**: Payload encryption (AES-256-CBC) and HMAC signing are handled seamlessly under the hood.
|
|
18
|
+
- **Built-in Polling Utilities**: Convenient `waitForCompletion` and `waitForWithdrawalCompletion` with live progress callbacks.
|
|
19
|
+
- **Universal ESM & CJS**: Full support for ES Modules (`import`), CommonJS (`require`), and TypeScript.
|
|
63
20
|
|
|
64
21
|
---
|
|
65
22
|
|
|
66
23
|
## 📦 Installation
|
|
67
24
|
|
|
68
|
-
### Node.js (Recommended)
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
npm install @patlink/node-api-client
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### With yarn
|
|
75
|
-
|
|
76
25
|
```bash
|
|
77
|
-
|
|
26
|
+
npm install @patalink/node-api-client
|
|
78
27
|
```
|
|
79
28
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
pnpm add @patlink/node-api-client
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
### Requirements
|
|
87
|
-
|
|
88
|
-
- Node.js 16.x or higher
|
|
89
|
-
- TypeScript 4.x or higher (optional)
|
|
29
|
+
*Requires Node.js 18.0.0 or higher.*
|
|
90
30
|
|
|
91
31
|
---
|
|
92
32
|
|
|
93
|
-
##
|
|
94
|
-
|
|
95
|
-
### Client Configuration Options
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
interface ClientConfig {
|
|
99
|
-
apiKey: string; // Required: Your API key from dashboard
|
|
100
|
-
encryptionKey: string; // Required: Your encryption key from dashboard
|
|
101
|
-
baseUrl: string; // Required: API endpoint URL
|
|
102
|
-
timeout?: number; // Optional: Request timeout in ms (default: 30000)
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Getting Your Keys
|
|
33
|
+
## 🔑 Authentication & Prerequisites
|
|
107
34
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
35
|
+
To use this SDK in production:
|
|
36
|
+
1. Create an account at **[patalink.me](https://patalink.me)**.
|
|
37
|
+
2. Complete **KYC identity verification** in your dashboard (required for API keys and automated payouts).
|
|
38
|
+
3. Navigate to **Dashboard → Settings → Security & API Keys** to copy:
|
|
39
|
+
- **API Key**: Starts with `pt_live_...` (or `pt_test_...` for sandbox)
|
|
40
|
+
- **Encryption Key**: 32-byte Base64-encoded string
|
|
112
41
|
|
|
113
|
-
|
|
42
|
+
Store these securely in your environment variables:
|
|
114
43
|
|
|
115
44
|
```env
|
|
116
|
-
PATALINK_API_KEY=
|
|
117
|
-
PATALINK_ENCRYPTION_KEY=
|
|
118
|
-
PATALINK_BASE_URL=https://patalink.me
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
```javascript
|
|
122
|
-
import { createClient } from '@patlink/node-api-client';
|
|
123
|
-
|
|
124
|
-
const client = createClient({
|
|
125
|
-
apiKey: process.env.PATALINK_API_KEY,
|
|
126
|
-
encryptionKey: process.env.PATALINK_ENCRYPTION_KEY,
|
|
127
|
-
baseUrl: process.env.PATALINK_BASE_URL,
|
|
128
|
-
timeout: 30000 // optional
|
|
129
|
-
});
|
|
45
|
+
PATALINK_API_KEY=pt_live_your_api_key_here
|
|
46
|
+
PATALINK_ENCRYPTION_KEY=your_base64_encryption_key_here
|
|
47
|
+
PATALINK_BASE_URL=https://api.patalink.me
|
|
130
48
|
```
|
|
131
49
|
|
|
132
50
|
---
|
|
133
51
|
|
|
134
|
-
##
|
|
135
|
-
|
|
136
|
-
### `createPayment(request)`
|
|
137
|
-
|
|
138
|
-
Create a new payment transaction.
|
|
52
|
+
## 🚀 Quick Start
|
|
139
53
|
|
|
140
|
-
|
|
54
|
+
Initialize the client once in your application:
|
|
141
55
|
|
|
142
56
|
```typescript
|
|
143
|
-
|
|
144
|
-
amount: number; // Required: Amount in RWF (min 100, max 2,000,000 for mobile money)
|
|
145
|
-
currency?: string; // Optional: Currency code (default: 'RWF')
|
|
146
|
-
paymentMethod: 'MTN' | 'Airtel' | 'PesaPal'; // Required: Payment method
|
|
147
|
-
phoneNumber?: string; // Required for MTN/Airtel
|
|
148
|
-
customerName?: string; // Optional: Customer's full name
|
|
149
|
-
customerEmail?: string; // Optional: Customer's email address
|
|
150
|
-
}
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Returns:**
|
|
57
|
+
import { createClient } from '@patalink/node-api-client';
|
|
154
58
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
redirectUrl?: string; // For PesaPal card payments
|
|
161
|
-
message?: string; // Additional response message
|
|
162
|
-
}
|
|
59
|
+
const client = createClient({
|
|
60
|
+
apiKey: process.env.PATALINK_API_KEY!,
|
|
61
|
+
encryptionKey: process.env.PATALINK_ENCRYPTION_KEY!,
|
|
62
|
+
baseUrl: process.env.PATALINK_BASE_URL || 'https://api.patalink.me'
|
|
63
|
+
});
|
|
163
64
|
```
|
|
164
65
|
|
|
165
|
-
|
|
166
|
-
|
|
66
|
+
*CommonJS syntax is also fully supported:*
|
|
167
67
|
```javascript
|
|
168
|
-
|
|
169
|
-
const payment = await client.createPayment({
|
|
170
|
-
amount: 25000,
|
|
171
|
-
paymentMethod: 'MTN',
|
|
172
|
-
phoneNumber: '0788888888',
|
|
173
|
-
customerName: 'John Doe',
|
|
174
|
-
customerEmail: 'john@example.com'
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Card Payment (PesaPal)
|
|
178
|
-
const cardPayment = await client.createPayment({
|
|
179
|
-
amount: 50000,
|
|
180
|
-
paymentMethod: 'PesaPal',
|
|
181
|
-
customerName: 'Jane Smith',
|
|
182
|
-
customerEmail: 'jane@example.com'
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Handle redirect for card payments
|
|
186
|
-
if (cardPayment.redirectUrl) {
|
|
187
|
-
// Redirect user to payment page
|
|
188
|
-
res.redirect(cardPayment.redirectUrl);
|
|
189
|
-
}
|
|
68
|
+
const { createClient } = require('@patalink/node-api-client');
|
|
190
69
|
```
|
|
191
70
|
|
|
192
71
|
---
|
|
193
72
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
Get the current status of a transaction.
|
|
73
|
+
## 📖 Usage Examples
|
|
197
74
|
|
|
198
|
-
|
|
75
|
+
### 1. Inbound Collections (Customer Payments)
|
|
199
76
|
|
|
200
|
-
|
|
201
|
-
|-----------|------|-------------|
|
|
202
|
-
| `transactionId` | `string` | Transaction ID from `createPayment` response |
|
|
203
|
-
|
|
204
|
-
**Returns:**
|
|
77
|
+
Trigger a payment prompt on the customer's phone (MTN MoMo) or generate a checkout redirect for card payments (PesaPal):
|
|
205
78
|
|
|
206
79
|
```typescript
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
```
|
|
80
|
+
// 1. Initiate payment request
|
|
81
|
+
const payment = await client.createPayment({
|
|
82
|
+
amount: 5000, // Amount in RWF (min 100 RWF)
|
|
83
|
+
paymentMethod: 'MTN', // 'MTN' or 'PesaPal'
|
|
84
|
+
phoneNumber: '0788820857', // Customer Mobile Money number
|
|
85
|
+
customerName: 'Alice Umutoni', // Optional
|
|
86
|
+
customerEmail: 'alice@example.com', // Optional
|
|
87
|
+
callbackUrl: 'https://example.com/done' // Required for Card payments
|
|
88
|
+
});
|
|
218
89
|
|
|
219
|
-
|
|
90
|
+
console.log('Payment Initiated! Transaction ID:', payment.transactionId);
|
|
220
91
|
|
|
221
|
-
|
|
222
|
-
const
|
|
92
|
+
// 2. Poll until customer confirms payment with their MoMo PIN
|
|
93
|
+
const verified = await client.waitForCompletion(payment.transactionId, {
|
|
94
|
+
maxAttempts: 30, // Poll up to 30 times
|
|
95
|
+
interval: 2500, // Check every 2.5 seconds
|
|
96
|
+
onProgress: (attempt, status) => {
|
|
97
|
+
console.log(`Poll #${attempt}: Status is ${status}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
223
100
|
|
|
224
|
-
if (
|
|
225
|
-
|
|
226
|
-
} else if (status.status === 'pending') {
|
|
227
|
-
console.log('Still waiting for confirmation...');
|
|
101
|
+
if (verified.status === 'success') {
|
|
102
|
+
console.log('✅ Payment confirmed! Deliver goods or grant access.');
|
|
228
103
|
} else {
|
|
229
|
-
|
|
104
|
+
console.log('❌ Payment failed or expired.');
|
|
230
105
|
}
|
|
231
106
|
```
|
|
232
107
|
|
|
233
108
|
---
|
|
234
109
|
|
|
235
|
-
|
|
110
|
+
### 2. Instant B2C Disbursements (Payouts)
|
|
236
111
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
```javascript
|
|
240
|
-
import express from 'express';
|
|
241
|
-
import { createClient } from '@patlink/node-api-client';
|
|
242
|
-
|
|
243
|
-
const app = express();
|
|
244
|
-
const client = createClient({
|
|
245
|
-
apiKey: process.env.PATALINK_API_KEY,
|
|
246
|
-
encryptionKey: process.env.PATALINK_ENCRYPTION_KEY,
|
|
247
|
-
baseUrl: process.env.PATALINK_BASE_URL
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
app.post('/api/create-payment', async (req, res) => {
|
|
251
|
-
try {
|
|
252
|
-
const { amount, phoneNumber, customerName, customerEmail } = req.body;
|
|
253
|
-
|
|
254
|
-
const payment = await client.createPayment({
|
|
255
|
-
amount,
|
|
256
|
-
paymentMethod: 'MTN',
|
|
257
|
-
phoneNumber,
|
|
258
|
-
customerName,
|
|
259
|
-
customerEmail
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
res.json({
|
|
263
|
-
success: true,
|
|
264
|
-
transactionId: payment.transactionId,
|
|
265
|
-
status: payment.status
|
|
266
|
-
});
|
|
267
|
-
} catch (error) {
|
|
268
|
-
res.status(400).json({
|
|
269
|
-
success: false,
|
|
270
|
-
error: error.message
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
app.get('/api/payment-status/:transactionId', async (req, res) => {
|
|
276
|
-
try {
|
|
277
|
-
const status = await client.getTransactionStatus(req.params.transactionId);
|
|
278
|
-
res.json(status);
|
|
279
|
-
} catch (error) {
|
|
280
|
-
res.status(404).json({ error: error.message });
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
app.listen(3000, () => {
|
|
285
|
-
console.log('Server running on port 3000');
|
|
286
|
-
});
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
### Polling Implementation
|
|
290
|
-
|
|
291
|
-
```javascript
|
|
292
|
-
async function pollPaymentStatus(transactionId, maxAttempts = 30, interval = 2000) {
|
|
293
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
294
|
-
console.log(`Checking status (attempt ${attempt}/${maxAttempts})...`);
|
|
295
|
-
|
|
296
|
-
const status = await client.getTransactionStatus(transactionId);
|
|
297
|
-
|
|
298
|
-
if (status.status === 'success') {
|
|
299
|
-
console.log('✅ Payment successful!');
|
|
300
|
-
return status;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (status.status === 'failed') {
|
|
304
|
-
console.log('❌ Payment failed!');
|
|
305
|
-
return status;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Wait before next attempt
|
|
309
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
throw new Error('Payment timeout - still pending after maximum attempts');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Usage
|
|
316
|
-
const transaction = await client.createPayment({
|
|
317
|
-
amount: 25000,
|
|
318
|
-
paymentMethod: 'MTN',
|
|
319
|
-
phoneNumber: '0788888888'
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const result = await pollPaymentStatus(transaction.transactionId);
|
|
323
|
-
console.log('Final status:', result.status);
|
|
324
|
-
```
|
|
325
|
-
|
|
326
|
-
### Next.js API Route
|
|
112
|
+
Send funds directly from your PataLink cleared wallet balance to any MTN Mobile Money number in Rwanda:
|
|
327
113
|
|
|
328
114
|
```typescript
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
115
|
+
// 1. Dispatch payout
|
|
116
|
+
const payout = await client.createWithdrawal({
|
|
117
|
+
amount: 25000, // Amount in RWF (min 1,000, max 2,000,000)
|
|
118
|
+
destinationNumber: '0788820857', // MTN Rwanda number (078... / 079...)
|
|
119
|
+
network: 'MTN', // Network (default: 'MTN')
|
|
120
|
+
reference: 'PAYOUT-ORD-9021', // Optional: Your internal reference
|
|
121
|
+
recipientNote: 'Weekly Creator Earnings',// Optional: Note to recipient
|
|
122
|
+
idempotencyKey: 'payout-uuid-v4' // Optional: Prevents duplicate transfers
|
|
337
123
|
});
|
|
338
124
|
|
|
339
|
-
|
|
340
|
-
try {
|
|
341
|
-
const body = await request.json();
|
|
342
|
-
|
|
343
|
-
const payment = await client.createPayment({
|
|
344
|
-
amount: body.amount,
|
|
345
|
-
paymentMethod: body.paymentMethod,
|
|
346
|
-
phoneNumber: body.phoneNumber,
|
|
347
|
-
customerName: body.customerName,
|
|
348
|
-
customerEmail: body.customerEmail
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
return NextResponse.json(payment);
|
|
352
|
-
} catch (error: any) {
|
|
353
|
-
return NextResponse.json(
|
|
354
|
-
{ error: error.message },
|
|
355
|
-
{ status: 400 }
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
export async function GET(request: Request) {
|
|
361
|
-
const { searchParams } = new URL(request.url);
|
|
362
|
-
const transactionId = searchParams.get('transactionId');
|
|
363
|
-
|
|
364
|
-
if (!transactionId) {
|
|
365
|
-
return NextResponse.json(
|
|
366
|
-
{ error: 'transactionId is required' },
|
|
367
|
-
{ status: 400 }
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
try {
|
|
372
|
-
const status = await client.getTransactionStatus(transactionId);
|
|
373
|
-
return NextResponse.json(status);
|
|
374
|
-
} catch (error: any) {
|
|
375
|
-
return NextResponse.json(
|
|
376
|
-
{ error: error.message },
|
|
377
|
-
{ status: 404 }
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
```
|
|
125
|
+
console.log('Payout Submitted:', payout.id, 'Status:', payout.status);
|
|
382
126
|
|
|
383
|
-
|
|
127
|
+
// 2. Poll until terminal status (paid or failed)
|
|
128
|
+
const finalPayout = await client.waitForWithdrawalCompletion(payout.id, {
|
|
129
|
+
maxAttempts: 30,
|
|
130
|
+
interval: 2000,
|
|
131
|
+
onProgress: (attempt, status) => {
|
|
132
|
+
console.log(`Disbursement Poll #${attempt}: ${status}`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
384
135
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const results = [];
|
|
388
|
-
|
|
389
|
-
for (const payment of payments) {
|
|
390
|
-
try {
|
|
391
|
-
const result = await client.createPayment(payment);
|
|
392
|
-
results.push({
|
|
393
|
-
success: true,
|
|
394
|
-
transactionId: result.transactionId,
|
|
395
|
-
amount: payment.amount,
|
|
396
|
-
phoneNumber: payment.phoneNumber
|
|
397
|
-
});
|
|
398
|
-
} catch (error) {
|
|
399
|
-
results.push({
|
|
400
|
-
success: false,
|
|
401
|
-
error: error.message,
|
|
402
|
-
amount: payment.amount,
|
|
403
|
-
phoneNumber: payment.phoneNumber
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Rate limiting - avoid overwhelming the API
|
|
408
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return results;
|
|
136
|
+
if (finalPayout.status === 'paid') {
|
|
137
|
+
console.log('✅ Funds delivered to recipient mobile wallet!');
|
|
412
138
|
}
|
|
413
|
-
|
|
414
|
-
// Usage
|
|
415
|
-
const payments = [
|
|
416
|
-
{ amount: 10000, paymentMethod: 'MTN', phoneNumber: '0788888888' },
|
|
417
|
-
{ amount: 25000, paymentMethod: 'Airtel', phoneNumber: '0789999999' },
|
|
418
|
-
{ amount: 5000, paymentMethod: 'MTN', phoneNumber: '0787777777' }
|
|
419
|
-
];
|
|
420
|
-
|
|
421
|
-
const results = await processBulkPayments(payments);
|
|
422
|
-
console.log(results);
|
|
423
139
|
```
|
|
424
140
|
|
|
425
141
|
---
|
|
426
142
|
|
|
427
|
-
|
|
143
|
+
### 3. Check Wallet Balances & Account Info
|
|
428
144
|
|
|
429
|
-
|
|
145
|
+
Verify your merchant cleared balance before initiating payouts:
|
|
430
146
|
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
const payment = await client.createPayment({
|
|
434
|
-
amount: 500,
|
|
435
|
-
paymentMethod: 'MTN',
|
|
436
|
-
phoneNumber: '0788888888'
|
|
437
|
-
});
|
|
438
|
-
} catch (error) {
|
|
439
|
-
switch (error.message) {
|
|
440
|
-
case 'Amount must be at least 100 RWF':
|
|
441
|
-
// Handle minimum amount error
|
|
442
|
-
break;
|
|
443
|
-
case 'Mobile Money limit is 2,000,000 RWF':
|
|
444
|
-
// Handle maximum amount error
|
|
445
|
-
break;
|
|
446
|
-
case 'phoneNumber is required for mobile money payments':
|
|
447
|
-
// Handle missing phone number
|
|
448
|
-
break;
|
|
449
|
-
case 'Request timeout':
|
|
450
|
-
// Handle timeout - implement retry logic
|
|
451
|
-
break;
|
|
452
|
-
default:
|
|
453
|
-
// Handle other errors
|
|
454
|
-
console.error('Payment failed:', error.message);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
```
|
|
147
|
+
```typescript
|
|
148
|
+
const account = await client.getAccountInfo();
|
|
458
149
|
|
|
459
|
-
|
|
150
|
+
console.log('User ID: ', account.id);
|
|
151
|
+
console.log('Available Cleared: ', account.availableBalance, 'RWF');
|
|
152
|
+
console.log('Total Gross Volume:', account.walletBalance, 'RWF');
|
|
153
|
+
console.log('Pending Funds: ', account.pendingBalance, 'RWF');
|
|
460
154
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
466
|
-
try {
|
|
467
|
-
return await client.createPayment(request);
|
|
468
|
-
} catch (error) {
|
|
469
|
-
lastError = error;
|
|
470
|
-
|
|
471
|
-
if (attempt === maxRetries) break;
|
|
472
|
-
|
|
473
|
-
// Exponential backoff: 1s, 2s, 4s
|
|
474
|
-
const delay = Math.pow(2, attempt - 1) * 1000;
|
|
475
|
-
console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms...`);
|
|
476
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
throw lastError;
|
|
155
|
+
// Example: Guard payout against insufficient balance
|
|
156
|
+
if (account.availableBalance < 50000) {
|
|
157
|
+
throw new Error('Insufficient cleared balance to perform payout');
|
|
481
158
|
}
|
|
482
|
-
|
|
483
|
-
// Usage
|
|
484
|
-
const payment = await createPaymentWithRetry({
|
|
485
|
-
amount: 25000,
|
|
486
|
-
paymentMethod: 'MTN',
|
|
487
|
-
phoneNumber: '0788888888'
|
|
488
|
-
});
|
|
489
159
|
```
|
|
490
160
|
|
|
491
161
|
---
|
|
492
162
|
|
|
493
|
-
|
|
163
|
+
### 4. Webhook Listener (Express / Node.js)
|
|
494
164
|
|
|
495
|
-
|
|
165
|
+
Configure a webhook endpoint in your server to receive asynchronous real-time events for collections and disbursements:
|
|
496
166
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
```typescript
|
|
500
|
-
import { createClient, PaymentRequest, PaymentResponse, TransactionStatus } from '@patlink/node-api-client';
|
|
167
|
+
```javascript
|
|
168
|
+
import express from 'express';
|
|
501
169
|
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
170
|
+
const app = express();
|
|
171
|
+
app.use(express.json());
|
|
172
|
+
|
|
173
|
+
app.post('/api/webhooks/patalink', (req, res) => {
|
|
174
|
+
const { event, data } = req.body;
|
|
175
|
+
|
|
176
|
+
// Handle Payout Events (Disbursements)
|
|
177
|
+
if (event === 'withdrawal.paid') {
|
|
178
|
+
console.log(`✅ Payout Delivered: ${data.withdrawalId} (${data.amount} RWF)`);
|
|
179
|
+
// Update order status in your database
|
|
180
|
+
} else if (event === 'withdrawal.failed') {
|
|
181
|
+
console.log(`❌ Payout Failed: ${data.withdrawalId} - Reason: ${data.failureReason}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Handle Collection Events (Inbound Payments)
|
|
185
|
+
else if (event === 'payment.completed' || req.body.status === 'success') {
|
|
186
|
+
const txnId = req.body.transactionId || data?.transactionId;
|
|
187
|
+
console.log(`💰 Inbound Payment Confirmed: ${txnId}`);
|
|
188
|
+
// Fulfill customer order
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Always respond with 200 OK immediately
|
|
192
|
+
res.status(200).send('OK');
|
|
506
193
|
});
|
|
507
194
|
|
|
508
|
-
|
|
509
|
-
const payment = await client.createPayment(request);
|
|
510
|
-
return payment;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
async function checkStatus(transactionId: string): Promise<TransactionStatus> {
|
|
514
|
-
const status = await client.getTransactionStatus(transactionId);
|
|
515
|
-
return status;
|
|
516
|
-
}
|
|
195
|
+
app.listen(3000, () => console.log('Webhook server running on port 3000'));
|
|
517
196
|
```
|
|
518
197
|
|
|
519
198
|
---
|
|
520
199
|
|
|
521
|
-
##
|
|
200
|
+
## 🧪 Sandbox & Testing
|
|
522
201
|
|
|
523
|
-
|
|
202
|
+
You can test full workflows in the sandbox without touching real money:
|
|
203
|
+
- Use an API key starting with `pt_test_...`.
|
|
204
|
+
- Use test phone number `0788820857`.
|
|
205
|
+
- **Withdrawal Simulation Conventions**:
|
|
206
|
+
- Amounts ending in `99` (e.g., `1099 RWF`) simulate a **failed disbursement**.
|
|
207
|
+
- Amounts ending in `55` (e.g., `1555 RWF`) stay **pending** for testing timeout handling.
|
|
208
|
+
- All other amounts automatically transition to **paid**.
|
|
524
209
|
|
|
525
|
-
|
|
526
|
-
<script type="module">
|
|
527
|
-
import 'https://esm.sh/@patlink/html-payment-modal';
|
|
528
|
-
</script>
|
|
529
|
-
|
|
530
|
-
<payment-modal
|
|
531
|
-
base-url="https://patalink.me"
|
|
532
|
-
api-key="your_api_key"
|
|
533
|
-
encryption-key="your_encryption_key"
|
|
534
|
-
></payment-modal>
|
|
535
|
-
```
|
|
210
|
+
Try out live requests in the interactive **[PataLink Developer Playground](https://patalink.me/developer/docs)** before writing code.
|
|
536
211
|
|
|
537
212
|
---
|
|
538
213
|
|
|
539
|
-
##
|
|
540
|
-
|
|
541
|
-
### Can I use this in the browser?
|
|
542
|
-
|
|
543
|
-
**No.** This package uses server-side encryption and should only be used in Node.js environments. For browser payments, use our HTML web component instead.
|
|
544
|
-
|
|
545
|
-
### How do I get my API and encryption keys?
|
|
546
|
-
|
|
547
|
-
1. Sign up at [dashboard.patalink.me](https://dashboard.patalink.me)
|
|
548
|
-
2. Go to **Settings** → **API Keys**
|
|
549
|
-
3. Click **Generate API Key** and **Generate Encryption Key**
|
|
550
|
-
4. Store both keys securely in environment variables
|
|
551
|
-
|
|
552
|
-
### What's the difference between API key and encryption key?
|
|
553
|
-
|
|
554
|
-
- **API Key**: Identifies your account (authentication)
|
|
555
|
-
- **Encryption Key**: Encrypts payment data (security)
|
|
556
|
-
|
|
557
|
-
Both are required and should be kept secret.
|
|
558
|
-
|
|
559
|
-
### What payment methods are supported?
|
|
560
|
-
|
|
561
|
-
- MTN Mobile Money
|
|
562
|
-
- Airtel Money
|
|
563
|
-
- Credit/Debit Cards (via PesaPal)
|
|
564
|
-
|
|
565
|
-
### What's the minimum and maximum payment amount?
|
|
566
|
-
|
|
567
|
-
- **Minimum**: 100 RWF
|
|
568
|
-
- **Maximum for Mobile Money**: 2,000,000 RWF
|
|
569
|
-
- **Maximum for Cards**: No limit (subject to card issuer)
|
|
570
|
-
|
|
571
|
-
### How do I handle card payment redirects?
|
|
572
|
-
|
|
573
|
-
For PesaPal card payments, the response includes a `redirectUrl`. You need to redirect your user to this URL to complete the payment.
|
|
574
|
-
|
|
575
|
-
```javascript
|
|
576
|
-
const payment = await client.createPayment({
|
|
577
|
-
amount: 50000,
|
|
578
|
-
paymentMethod: 'PesaPal',
|
|
579
|
-
customerEmail: 'customer@example.com'
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
if (payment.redirectUrl) {
|
|
583
|
-
res.redirect(payment.redirectUrl);
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Is the encryption automatic?
|
|
588
|
-
|
|
589
|
-
**Yes.** The client automatically encrypts all payment data before sending to our API. You don't need to implement any encryption logic.
|
|
590
|
-
|
|
591
|
-
### Can I test without real money?
|
|
592
|
-
|
|
593
|
-
**Yes.** Use your test keys from the dashboard (they start with `pk_test_`). Test transactions are processed in sandbox mode.
|
|
214
|
+
## 📚 API Reference
|
|
594
215
|
|
|
595
|
-
###
|
|
216
|
+
### `createClient(config: ClientConfig): PataLinkClient`
|
|
596
217
|
|
|
597
|
-
|
|
218
|
+
| Option | Type | Required | Description |
|
|
219
|
+
|---|---|---|---|
|
|
220
|
+
| `apiKey` | `string` | Yes | Your PataLink API key (`pt_live_...` or `pt_test_...`) |
|
|
221
|
+
| `encryptionKey` | `string` | Yes | 32-byte Base64 encryption key from dashboard |
|
|
222
|
+
| `baseUrl` | `string` | No | API endpoint (defaults to `https://api.patalink.me`) |
|
|
223
|
+
| `timeout` | `number` | No | Request timeout in milliseconds (default: 30000) |
|
|
598
224
|
|
|
599
|
-
###
|
|
225
|
+
### Client Methods
|
|
600
226
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
227
|
+
- `client.createPayment(request: PaymentRequest): Promise<PaymentResponse>`
|
|
228
|
+
- `client.getTransactionStatus(transactionId: string): Promise<TransactionStatus>`
|
|
229
|
+
- `client.waitForCompletion(transactionId: string, options?): Promise<TransactionStatus>`
|
|
230
|
+
- `client.createWithdrawal(request: WithdrawalRequest): Promise<WithdrawalResponse>`
|
|
231
|
+
- `client.getWithdrawalStatus(withdrawalId: string): Promise<WithdrawalStatus>`
|
|
232
|
+
- `client.waitForWithdrawalCompletion(withdrawalId: string, options?): Promise<WithdrawalStatus>`
|
|
233
|
+
- `client.getAccountInfo(): Promise<AccountInfo>`
|
|
234
|
+
- `client.listTransactions(options?): Promise<{ transactions, total }>`
|
|
235
|
+
- `client.listWithdrawals(options?): Promise<{ withdrawals, total }>`
|
|
604
236
|
|
|
605
237
|
---
|
|
606
238
|
|
|
607
|
-
##
|
|
239
|
+
## 🔒 Security
|
|
608
240
|
|
|
609
|
-
-
|
|
610
|
-
- 💬 [Discord Community](https://discord.gg/patlink)
|
|
611
|
-
- 🐛 [GitHub Issues](https://github.com/patlink/api-client/issues)
|
|
612
|
-
- ✉️ [support@patalink.me](mailto:support@patalink.me)
|
|
241
|
+
All transaction initiation requests to PataLink are protected by **AES-256-CBC payload encryption** and signed with an **HMAC-SHA256 signature** generated using your secret encryption key. This SDK handles encryption and signing automatically for every request.
|
|
613
242
|
|
|
614
243
|
---
|
|
615
244
|
|
|
616
245
|
## 📄 License
|
|
617
246
|
|
|
618
247
|
MIT © [PataLink](https://patalink.me)
|
|
619
|
-
|
|
620
|
-
---
|
|
621
|
-
|
|
622
|
-
**Secure, encrypted payments for your Node.js backend.** 🚀
|
|
623
|
-
```
|
package/dist/index.d.mts
CHANGED
|
@@ -24,6 +24,43 @@ interface TransactionStatus {
|
|
|
24
24
|
completedAt?: string;
|
|
25
25
|
providerRef?: string;
|
|
26
26
|
}
|
|
27
|
+
interface WithdrawalRequest {
|
|
28
|
+
amount: number;
|
|
29
|
+
destinationNumber?: string;
|
|
30
|
+
phoneNumber?: string;
|
|
31
|
+
network?: 'MTN';
|
|
32
|
+
reference?: string;
|
|
33
|
+
recipientNote?: string;
|
|
34
|
+
callbackUrl?: string;
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
}
|
|
37
|
+
interface WithdrawalResponse {
|
|
38
|
+
id: string;
|
|
39
|
+
withdrawalId: string;
|
|
40
|
+
status: 'processing' | 'pending' | 'paid' | 'rejected' | 'failed';
|
|
41
|
+
amount: number;
|
|
42
|
+
feeAmount: number;
|
|
43
|
+
netAmount: number;
|
|
44
|
+
currency: string;
|
|
45
|
+
destinationNumber: string;
|
|
46
|
+
network: 'MTN';
|
|
47
|
+
reference?: string;
|
|
48
|
+
providerRef?: string;
|
|
49
|
+
externalId?: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
}
|
|
52
|
+
interface WithdrawalStatus {
|
|
53
|
+
id: string;
|
|
54
|
+
status: 'processing' | 'pending' | 'paid' | 'rejected' | 'failed';
|
|
55
|
+
amount: number;
|
|
56
|
+
destinationNumber: string;
|
|
57
|
+
network: 'MTN';
|
|
58
|
+
reference?: string;
|
|
59
|
+
providerRef?: string;
|
|
60
|
+
reason?: string;
|
|
61
|
+
processedAt?: string;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
}
|
|
27
64
|
interface ClientConfig {
|
|
28
65
|
apiKey: string;
|
|
29
66
|
encryptionKey: string;
|
|
@@ -89,17 +126,21 @@ declare class PataLinkClient {
|
|
|
89
126
|
availableBalance: number;
|
|
90
127
|
}>;
|
|
91
128
|
/**
|
|
92
|
-
* Initiate withdrawal
|
|
129
|
+
* Initiate a withdrawal / payout (MTN Mobile Money)
|
|
93
130
|
*/
|
|
94
|
-
createWithdrawal(request:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
createWithdrawal(request: WithdrawalRequest): Promise<WithdrawalResponse>;
|
|
132
|
+
/**
|
|
133
|
+
* Get withdrawal status by ID, external ID, or reference
|
|
134
|
+
*/
|
|
135
|
+
getWithdrawalStatus(withdrawalId: string): Promise<WithdrawalStatus>;
|
|
136
|
+
/**
|
|
137
|
+
* Poll until withdrawal reaches a terminal status (paid, rejected, or failed)
|
|
138
|
+
*/
|
|
139
|
+
waitForWithdrawalCompletion(withdrawalId: string, options?: {
|
|
140
|
+
maxAttempts?: number;
|
|
141
|
+
interval?: number;
|
|
142
|
+
onProgress?: (attempt: number, status: string) => void;
|
|
143
|
+
}): Promise<WithdrawalStatus>;
|
|
103
144
|
/**
|
|
104
145
|
* List withdrawals
|
|
105
146
|
*/
|
|
@@ -113,4 +154,4 @@ declare class PataLinkClient {
|
|
|
113
154
|
}
|
|
114
155
|
declare function createClient(config: ClientConfig): PataLinkClient;
|
|
115
156
|
|
|
116
|
-
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
|
|
157
|
+
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, type WithdrawalRequest, type WithdrawalResponse, type WithdrawalStatus, createClient, PataLinkClient as default };
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,43 @@ interface TransactionStatus {
|
|
|
24
24
|
completedAt?: string;
|
|
25
25
|
providerRef?: string;
|
|
26
26
|
}
|
|
27
|
+
interface WithdrawalRequest {
|
|
28
|
+
amount: number;
|
|
29
|
+
destinationNumber?: string;
|
|
30
|
+
phoneNumber?: string;
|
|
31
|
+
network?: 'MTN';
|
|
32
|
+
reference?: string;
|
|
33
|
+
recipientNote?: string;
|
|
34
|
+
callbackUrl?: string;
|
|
35
|
+
idempotencyKey?: string;
|
|
36
|
+
}
|
|
37
|
+
interface WithdrawalResponse {
|
|
38
|
+
id: string;
|
|
39
|
+
withdrawalId: string;
|
|
40
|
+
status: 'processing' | 'pending' | 'paid' | 'rejected' | 'failed';
|
|
41
|
+
amount: number;
|
|
42
|
+
feeAmount: number;
|
|
43
|
+
netAmount: number;
|
|
44
|
+
currency: string;
|
|
45
|
+
destinationNumber: string;
|
|
46
|
+
network: 'MTN';
|
|
47
|
+
reference?: string;
|
|
48
|
+
providerRef?: string;
|
|
49
|
+
externalId?: string;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
}
|
|
52
|
+
interface WithdrawalStatus {
|
|
53
|
+
id: string;
|
|
54
|
+
status: 'processing' | 'pending' | 'paid' | 'rejected' | 'failed';
|
|
55
|
+
amount: number;
|
|
56
|
+
destinationNumber: string;
|
|
57
|
+
network: 'MTN';
|
|
58
|
+
reference?: string;
|
|
59
|
+
providerRef?: string;
|
|
60
|
+
reason?: string;
|
|
61
|
+
processedAt?: string;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
}
|
|
27
64
|
interface ClientConfig {
|
|
28
65
|
apiKey: string;
|
|
29
66
|
encryptionKey: string;
|
|
@@ -89,17 +126,21 @@ declare class PataLinkClient {
|
|
|
89
126
|
availableBalance: number;
|
|
90
127
|
}>;
|
|
91
128
|
/**
|
|
92
|
-
* Initiate withdrawal
|
|
129
|
+
* Initiate a withdrawal / payout (MTN Mobile Money)
|
|
93
130
|
*/
|
|
94
|
-
createWithdrawal(request:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
131
|
+
createWithdrawal(request: WithdrawalRequest): Promise<WithdrawalResponse>;
|
|
132
|
+
/**
|
|
133
|
+
* Get withdrawal status by ID, external ID, or reference
|
|
134
|
+
*/
|
|
135
|
+
getWithdrawalStatus(withdrawalId: string): Promise<WithdrawalStatus>;
|
|
136
|
+
/**
|
|
137
|
+
* Poll until withdrawal reaches a terminal status (paid, rejected, or failed)
|
|
138
|
+
*/
|
|
139
|
+
waitForWithdrawalCompletion(withdrawalId: string, options?: {
|
|
140
|
+
maxAttempts?: number;
|
|
141
|
+
interval?: number;
|
|
142
|
+
onProgress?: (attempt: number, status: string) => void;
|
|
143
|
+
}): Promise<WithdrawalStatus>;
|
|
103
144
|
/**
|
|
104
145
|
* List withdrawals
|
|
105
146
|
*/
|
|
@@ -113,4 +154,4 @@ declare class PataLinkClient {
|
|
|
113
154
|
}
|
|
114
155
|
declare function createClient(config: ClientConfig): PataLinkClient;
|
|
115
156
|
|
|
116
|
-
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, createClient, PataLinkClient as default };
|
|
157
|
+
export { type ClientConfig, type PaymentRequest, type PaymentResponse, type TransactionStatus, type WithdrawalRequest, type WithdrawalResponse, type WithdrawalStatus, createClient, PataLinkClient as default };
|
package/dist/index.js
CHANGED
|
@@ -34,7 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
default: () => index_default
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(index_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_crypto_js = __toESM(require("crypto-js"));
|
|
38
38
|
var PataLinkClient = class {
|
|
39
39
|
constructor(config) {
|
|
40
40
|
if (!config.apiKey) throw new Error("API key is required");
|
|
@@ -52,20 +52,20 @@ var PataLinkClient = class {
|
|
|
52
52
|
const dataString = JSON.stringify(data);
|
|
53
53
|
let keyWA;
|
|
54
54
|
try {
|
|
55
|
-
keyWA =
|
|
55
|
+
keyWA = import_crypto_js.default.enc.Base64.parse(this.encryptionKey);
|
|
56
56
|
} catch (error) {
|
|
57
|
-
keyWA =
|
|
57
|
+
keyWA = import_crypto_js.default.enc.Utf8.parse(this.encryptionKey);
|
|
58
58
|
}
|
|
59
|
-
const iv =
|
|
60
|
-
const ivHex = iv.toString(
|
|
61
|
-
const ivWA =
|
|
62
|
-
const encrypted =
|
|
59
|
+
const iv = import_crypto_js.default.lib.WordArray.random(16);
|
|
60
|
+
const ivHex = iv.toString(import_crypto_js.default.enc.Hex);
|
|
61
|
+
const ivWA = import_crypto_js.default.enc.Hex.parse(ivHex);
|
|
62
|
+
const encrypted = import_crypto_js.default.AES.encrypt(dataString, keyWA, {
|
|
63
63
|
iv: ivWA,
|
|
64
|
-
mode:
|
|
65
|
-
padding:
|
|
64
|
+
mode: import_crypto_js.default.mode.CBC,
|
|
65
|
+
padding: import_crypto_js.default.pad.Pkcs7
|
|
66
66
|
});
|
|
67
|
-
const encryptedBase64 = encrypted.ciphertext.toString(
|
|
68
|
-
const hmac =
|
|
67
|
+
const encryptedBase64 = encrypted.ciphertext.toString(import_crypto_js.default.enc.Base64);
|
|
68
|
+
const hmac = import_crypto_js.default.HmacSHA256(dataString, keyWA).toString();
|
|
69
69
|
return {
|
|
70
70
|
encrypted: encryptedBase64,
|
|
71
71
|
iv: ivHex,
|
|
@@ -132,7 +132,7 @@ var PataLinkClient = class {
|
|
|
132
132
|
paymentData.callbackUrl = request.callbackUrl;
|
|
133
133
|
}
|
|
134
134
|
const encryptedPayload = this.encrypt(paymentData);
|
|
135
|
-
const response = await this.request("/
|
|
135
|
+
const response = await this.request("/pay", {
|
|
136
136
|
method: "POST",
|
|
137
137
|
headers: {
|
|
138
138
|
"x-encrypted": "true"
|
|
@@ -146,7 +146,7 @@ var PataLinkClient = class {
|
|
|
146
146
|
*/
|
|
147
147
|
async getTransactionStatus(transactionId) {
|
|
148
148
|
if (!transactionId) throw new Error("transactionId is required");
|
|
149
|
-
return await this.request(`/
|
|
149
|
+
return await this.request(`/pay/${transactionId}`, {
|
|
150
150
|
method: "GET"
|
|
151
151
|
});
|
|
152
152
|
}
|
|
@@ -159,8 +159,8 @@ var PataLinkClient = class {
|
|
|
159
159
|
let attempts = 0;
|
|
160
160
|
while (attempts < maxAttempts) {
|
|
161
161
|
attempts++;
|
|
162
|
-
options?.onProgress?.(attempts, "checking");
|
|
163
162
|
const status = await this.getTransactionStatus(transactionId);
|
|
163
|
+
options?.onProgress?.(attempts, status.status);
|
|
164
164
|
if (status.status === "success" || status.status === "failed") {
|
|
165
165
|
return status;
|
|
166
166
|
}
|
|
@@ -177,7 +177,7 @@ var PataLinkClient = class {
|
|
|
177
177
|
if (options?.offset) params.append("offset", options.offset.toString());
|
|
178
178
|
if (options?.status) params.append("status", options.status);
|
|
179
179
|
const query = params.toString();
|
|
180
|
-
const endpoint = `/
|
|
180
|
+
const endpoint = `/transactions${query ? `?${query}` : ""}`;
|
|
181
181
|
return this.request(endpoint, {
|
|
182
182
|
method: "GET"
|
|
183
183
|
});
|
|
@@ -186,7 +186,7 @@ var PataLinkClient = class {
|
|
|
186
186
|
* Rotate encryption key (generates new key, revokes old one after 24h)
|
|
187
187
|
*/
|
|
188
188
|
async rotateEncryptionKey() {
|
|
189
|
-
return this.request("/
|
|
189
|
+
return this.request("/keys/rotate", {
|
|
190
190
|
method: "POST"
|
|
191
191
|
});
|
|
192
192
|
}
|
|
@@ -194,24 +194,67 @@ var PataLinkClient = class {
|
|
|
194
194
|
* Get account information
|
|
195
195
|
*/
|
|
196
196
|
async getAccountInfo() {
|
|
197
|
-
return this.request("/
|
|
197
|
+
return this.request("/account", { method: "GET" });
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
|
-
* Initiate withdrawal
|
|
200
|
+
* Initiate a withdrawal / payout (MTN Mobile Money)
|
|
201
201
|
*/
|
|
202
202
|
async createWithdrawal(request) {
|
|
203
|
+
const destination = request.destinationNumber || request.phoneNumber;
|
|
203
204
|
if (!request.amount) throw new Error("amount is required");
|
|
204
|
-
if (!
|
|
205
|
-
if (request.amount <
|
|
205
|
+
if (!destination) throw new Error("destinationNumber is required");
|
|
206
|
+
if (request.amount < 1e3) throw new Error("Minimum withdrawal is 1,000 RWF");
|
|
206
207
|
if (request.amount > 2e6) throw new Error("Maximum withdrawal is 2,000,000 RWF");
|
|
207
|
-
const
|
|
208
|
-
|
|
208
|
+
const withdrawalData = {
|
|
209
|
+
amount: request.amount,
|
|
210
|
+
destinationNumber: destination,
|
|
211
|
+
network: request.network || "MTN",
|
|
212
|
+
...request.reference && { reference: request.reference },
|
|
213
|
+
...request.recipientNote && { recipientNote: request.recipientNote },
|
|
214
|
+
...request.callbackUrl && { callbackUrl: request.callbackUrl },
|
|
215
|
+
...request.idempotencyKey && { idempotencyKey: request.idempotencyKey }
|
|
216
|
+
};
|
|
217
|
+
const encryptedPayload = this.encrypt(withdrawalData);
|
|
218
|
+
const headers = {
|
|
219
|
+
"x-encrypted": "true"
|
|
220
|
+
};
|
|
221
|
+
if (request.idempotencyKey) {
|
|
222
|
+
headers["Idempotency-Key"] = request.idempotencyKey;
|
|
223
|
+
}
|
|
224
|
+
const response = await this.request("/withdraw", {
|
|
209
225
|
method: "POST",
|
|
210
|
-
headers
|
|
211
|
-
"x-encrypted": "true"
|
|
212
|
-
},
|
|
226
|
+
headers,
|
|
213
227
|
body: JSON.stringify(encryptedPayload)
|
|
214
228
|
});
|
|
229
|
+
response.withdrawalId = response.id;
|
|
230
|
+
return response;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Get withdrawal status by ID, external ID, or reference
|
|
234
|
+
*/
|
|
235
|
+
async getWithdrawalStatus(withdrawalId) {
|
|
236
|
+
if (!withdrawalId) throw new Error("withdrawalId is required");
|
|
237
|
+
return await this.request(`/withdraw/${withdrawalId}`, {
|
|
238
|
+
method: "GET"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Poll until withdrawal reaches a terminal status (paid, rejected, or failed)
|
|
243
|
+
*/
|
|
244
|
+
async waitForWithdrawalCompletion(withdrawalId, options) {
|
|
245
|
+
const maxAttempts = options?.maxAttempts || 60;
|
|
246
|
+
const interval = options?.interval || 3e3;
|
|
247
|
+
let attempts = 0;
|
|
248
|
+
while (attempts < maxAttempts) {
|
|
249
|
+
attempts++;
|
|
250
|
+
const status = await this.getWithdrawalStatus(withdrawalId);
|
|
251
|
+
options?.onProgress?.(attempts, status.status);
|
|
252
|
+
if (status.status === "paid" || status.status === "rejected" || status.status === "failed") {
|
|
253
|
+
return status;
|
|
254
|
+
}
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
256
|
+
}
|
|
257
|
+
throw new Error("Withdrawal timeout - still processing after maximum attempts");
|
|
215
258
|
}
|
|
216
259
|
/**
|
|
217
260
|
* List withdrawals
|
|
@@ -221,7 +264,7 @@ var PataLinkClient = class {
|
|
|
221
264
|
if (options?.limit) params.append("limit", options.limit.toString());
|
|
222
265
|
if (options?.offset) params.append("offset", options.offset.toString());
|
|
223
266
|
const query = params.toString();
|
|
224
|
-
const endpoint = `/
|
|
267
|
+
const endpoint = `/withdrawals${query ? `?${query}` : ""}`;
|
|
225
268
|
return this.request(endpoint, { method: "GET" });
|
|
226
269
|
}
|
|
227
270
|
};
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import
|
|
2
|
+
import CryptoJS from "crypto-js";
|
|
3
3
|
var PataLinkClient = class {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
if (!config.apiKey) throw new Error("API key is required");
|
|
@@ -97,7 +97,7 @@ var PataLinkClient = class {
|
|
|
97
97
|
paymentData.callbackUrl = request.callbackUrl;
|
|
98
98
|
}
|
|
99
99
|
const encryptedPayload = this.encrypt(paymentData);
|
|
100
|
-
const response = await this.request("/
|
|
100
|
+
const response = await this.request("/pay", {
|
|
101
101
|
method: "POST",
|
|
102
102
|
headers: {
|
|
103
103
|
"x-encrypted": "true"
|
|
@@ -111,7 +111,7 @@ var PataLinkClient = class {
|
|
|
111
111
|
*/
|
|
112
112
|
async getTransactionStatus(transactionId) {
|
|
113
113
|
if (!transactionId) throw new Error("transactionId is required");
|
|
114
|
-
return await this.request(`/
|
|
114
|
+
return await this.request(`/pay/${transactionId}`, {
|
|
115
115
|
method: "GET"
|
|
116
116
|
});
|
|
117
117
|
}
|
|
@@ -124,8 +124,8 @@ var PataLinkClient = class {
|
|
|
124
124
|
let attempts = 0;
|
|
125
125
|
while (attempts < maxAttempts) {
|
|
126
126
|
attempts++;
|
|
127
|
-
options?.onProgress?.(attempts, "checking");
|
|
128
127
|
const status = await this.getTransactionStatus(transactionId);
|
|
128
|
+
options?.onProgress?.(attempts, status.status);
|
|
129
129
|
if (status.status === "success" || status.status === "failed") {
|
|
130
130
|
return status;
|
|
131
131
|
}
|
|
@@ -142,7 +142,7 @@ var PataLinkClient = class {
|
|
|
142
142
|
if (options?.offset) params.append("offset", options.offset.toString());
|
|
143
143
|
if (options?.status) params.append("status", options.status);
|
|
144
144
|
const query = params.toString();
|
|
145
|
-
const endpoint = `/
|
|
145
|
+
const endpoint = `/transactions${query ? `?${query}` : ""}`;
|
|
146
146
|
return this.request(endpoint, {
|
|
147
147
|
method: "GET"
|
|
148
148
|
});
|
|
@@ -151,7 +151,7 @@ var PataLinkClient = class {
|
|
|
151
151
|
* Rotate encryption key (generates new key, revokes old one after 24h)
|
|
152
152
|
*/
|
|
153
153
|
async rotateEncryptionKey() {
|
|
154
|
-
return this.request("/
|
|
154
|
+
return this.request("/keys/rotate", {
|
|
155
155
|
method: "POST"
|
|
156
156
|
});
|
|
157
157
|
}
|
|
@@ -159,24 +159,67 @@ var PataLinkClient = class {
|
|
|
159
159
|
* Get account information
|
|
160
160
|
*/
|
|
161
161
|
async getAccountInfo() {
|
|
162
|
-
return this.request("/
|
|
162
|
+
return this.request("/account", { method: "GET" });
|
|
163
163
|
}
|
|
164
164
|
/**
|
|
165
|
-
* Initiate withdrawal
|
|
165
|
+
* Initiate a withdrawal / payout (MTN Mobile Money)
|
|
166
166
|
*/
|
|
167
167
|
async createWithdrawal(request) {
|
|
168
|
+
const destination = request.destinationNumber || request.phoneNumber;
|
|
168
169
|
if (!request.amount) throw new Error("amount is required");
|
|
169
|
-
if (!
|
|
170
|
-
if (request.amount <
|
|
170
|
+
if (!destination) throw new Error("destinationNumber is required");
|
|
171
|
+
if (request.amount < 1e3) throw new Error("Minimum withdrawal is 1,000 RWF");
|
|
171
172
|
if (request.amount > 2e6) throw new Error("Maximum withdrawal is 2,000,000 RWF");
|
|
172
|
-
const
|
|
173
|
-
|
|
173
|
+
const withdrawalData = {
|
|
174
|
+
amount: request.amount,
|
|
175
|
+
destinationNumber: destination,
|
|
176
|
+
network: request.network || "MTN",
|
|
177
|
+
...request.reference && { reference: request.reference },
|
|
178
|
+
...request.recipientNote && { recipientNote: request.recipientNote },
|
|
179
|
+
...request.callbackUrl && { callbackUrl: request.callbackUrl },
|
|
180
|
+
...request.idempotencyKey && { idempotencyKey: request.idempotencyKey }
|
|
181
|
+
};
|
|
182
|
+
const encryptedPayload = this.encrypt(withdrawalData);
|
|
183
|
+
const headers = {
|
|
184
|
+
"x-encrypted": "true"
|
|
185
|
+
};
|
|
186
|
+
if (request.idempotencyKey) {
|
|
187
|
+
headers["Idempotency-Key"] = request.idempotencyKey;
|
|
188
|
+
}
|
|
189
|
+
const response = await this.request("/withdraw", {
|
|
174
190
|
method: "POST",
|
|
175
|
-
headers
|
|
176
|
-
"x-encrypted": "true"
|
|
177
|
-
},
|
|
191
|
+
headers,
|
|
178
192
|
body: JSON.stringify(encryptedPayload)
|
|
179
193
|
});
|
|
194
|
+
response.withdrawalId = response.id;
|
|
195
|
+
return response;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get withdrawal status by ID, external ID, or reference
|
|
199
|
+
*/
|
|
200
|
+
async getWithdrawalStatus(withdrawalId) {
|
|
201
|
+
if (!withdrawalId) throw new Error("withdrawalId is required");
|
|
202
|
+
return await this.request(`/withdraw/${withdrawalId}`, {
|
|
203
|
+
method: "GET"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Poll until withdrawal reaches a terminal status (paid, rejected, or failed)
|
|
208
|
+
*/
|
|
209
|
+
async waitForWithdrawalCompletion(withdrawalId, options) {
|
|
210
|
+
const maxAttempts = options?.maxAttempts || 60;
|
|
211
|
+
const interval = options?.interval || 3e3;
|
|
212
|
+
let attempts = 0;
|
|
213
|
+
while (attempts < maxAttempts) {
|
|
214
|
+
attempts++;
|
|
215
|
+
const status = await this.getWithdrawalStatus(withdrawalId);
|
|
216
|
+
options?.onProgress?.(attempts, status.status);
|
|
217
|
+
if (status.status === "paid" || status.status === "rejected" || status.status === "failed") {
|
|
218
|
+
return status;
|
|
219
|
+
}
|
|
220
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
221
|
+
}
|
|
222
|
+
throw new Error("Withdrawal timeout - still processing after maximum attempts");
|
|
180
223
|
}
|
|
181
224
|
/**
|
|
182
225
|
* List withdrawals
|
|
@@ -186,7 +229,7 @@ var PataLinkClient = class {
|
|
|
186
229
|
if (options?.limit) params.append("limit", options.limit.toString());
|
|
187
230
|
if (options?.offset) params.append("offset", options.offset.toString());
|
|
188
231
|
const query = params.toString();
|
|
189
|
-
const endpoint = `/
|
|
232
|
+
const endpoint = `/withdrawals${query ? `?${query}` : ""}`;
|
|
190
233
|
return this.request(endpoint, { method: "GET" });
|
|
191
234
|
}
|
|
192
235
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patalink/node-api-client",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Official PataLink API client - Simple, secure payment integration",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
"require": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
-
"files": [
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
16
18
|
"scripts": {
|
|
17
19
|
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
18
20
|
"test": "jest",
|
|
@@ -40,4 +42,4 @@
|
|
|
40
42
|
"dependencies": {
|
|
41
43
|
"crypto-js": "^4.2.0"
|
|
42
44
|
}
|
|
43
|
-
}
|
|
45
|
+
}
|