blaaiz-nodejs-sdk 1.0.3 → 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 +258 -44
- package/package.json +1 -1
- package/src/services/CollectionService.js +9 -1
- package/src/services/CustomerService.js +26 -2
- package/src/services/FeesService.js +6 -1
- package/src/services/PayoutService.js +46 -10
- package/src/services/VirtualBankAccountService.js +49 -3
- package/src/services/WebhookService.js +15 -7
package/README.md
CHANGED
|
@@ -32,12 +32,12 @@ console.log('API Connected:', isConnected);
|
|
|
32
32
|
## Features
|
|
33
33
|
|
|
34
34
|
- **Customer Management**: Create, update, and manage customers with KYC verification
|
|
35
|
-
- **Collections**: Support for multiple collection methods (Open Banking, Card, Crypto, Bank Transfer)
|
|
36
|
-
- **Payouts**: Bank transfers and
|
|
35
|
+
- **Collections**: Support for multiple collection methods (Open Banking, Card, Crypto, Bank Transfer, Interac)
|
|
36
|
+
- **Payouts**: Bank transfers, Interac, ACH, Wire, and Crypto payouts across multiple currencies
|
|
37
37
|
- **Virtual Bank Accounts**: Create and manage virtual accounts for NGN collections
|
|
38
38
|
- **Wallets**: Multi-currency wallet management
|
|
39
39
|
- **Transactions**: Transaction history and status tracking
|
|
40
|
-
- **Webhooks**: Webhook configuration and management
|
|
40
|
+
- **Webhooks**: Webhook configuration and management with signature verification
|
|
41
41
|
- **Files**: Document upload with pre-signed URLs
|
|
42
42
|
- **Fees**: Real-time fee calculations and breakdowns
|
|
43
43
|
- **Banks & Currencies**: Access to supported banks and currencies
|
|
@@ -51,8 +51,11 @@ console.log('API Connected:', isConnected);
|
|
|
51
51
|
- **EUR/GBP**: Open Banking
|
|
52
52
|
|
|
53
53
|
### Payouts
|
|
54
|
-
- **Bank Transfer**:
|
|
54
|
+
- **Bank Transfer**: NGN, GBP, EUR
|
|
55
55
|
- **Interac**: CAD transactions
|
|
56
|
+
- **ACH**: USD transactions
|
|
57
|
+
- **Wire**: USD transactions
|
|
58
|
+
- **Crypto**: USDT, USDC on multiple networks
|
|
56
59
|
|
|
57
60
|
## API Reference
|
|
58
61
|
|
|
@@ -75,6 +78,8 @@ const customer = await blaaiz.customers.create({
|
|
|
75
78
|
console.log('Customer ID:', customer.data.data.id);
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
> **Note**: For `individual` type, `first_name` and `last_name` are required. For `business` type, `business_name` is required instead.
|
|
82
|
+
|
|
78
83
|
#### Get Customer
|
|
79
84
|
|
|
80
85
|
```javascript
|
|
@@ -98,6 +103,20 @@ const updatedCustomer = await blaaiz.customers.update('customer-id', {
|
|
|
98
103
|
});
|
|
99
104
|
```
|
|
100
105
|
|
|
106
|
+
#### List Customer Beneficiaries
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
const beneficiaries = await blaaiz.customers.listBeneficiaries('customer-id');
|
|
110
|
+
console.log('Beneficiaries:', beneficiaries.data);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Get Specific Beneficiary
|
|
114
|
+
|
|
115
|
+
```javascript
|
|
116
|
+
const beneficiary = await blaaiz.customers.getBeneficiary('customer-id', 'beneficiary-id');
|
|
117
|
+
console.log('Beneficiary:', beneficiary.data);
|
|
118
|
+
```
|
|
119
|
+
|
|
101
120
|
### File Management & KYC
|
|
102
121
|
|
|
103
122
|
#### Upload Customer Documents
|
|
@@ -163,11 +182,16 @@ const fileAssociation = await blaaiz.customers.uploadFiles('customer-id', {
|
|
|
163
182
|
|
|
164
183
|
```javascript
|
|
165
184
|
const collection = await blaaiz.collections.initiate({
|
|
166
|
-
method: "open_banking",
|
|
167
|
-
amount: 100.00,
|
|
168
185
|
customer_id: "customer-id",
|
|
169
186
|
wallet_id: "wallet-id",
|
|
170
|
-
|
|
187
|
+
amount: 100.00,
|
|
188
|
+
currency: "EUR", // EUR, GBP, NGN, USD
|
|
189
|
+
method: "open_banking",
|
|
190
|
+
phone_number: "+1234567890", // Optional
|
|
191
|
+
email: "customer@example.com", // Optional
|
|
192
|
+
reference: "your-reference", // Optional
|
|
193
|
+
narration: "Payment description", // Optional
|
|
194
|
+
redirect_url: "https://your-site.com/callback" // Optional
|
|
171
195
|
});
|
|
172
196
|
|
|
173
197
|
console.log('Payment URL:', collection.data.url);
|
|
@@ -178,15 +202,34 @@ console.log('Transaction ID:', collection.data.transaction_id);
|
|
|
178
202
|
|
|
179
203
|
```javascript
|
|
180
204
|
const collection = await blaaiz.collections.initiate({
|
|
181
|
-
method: "card",
|
|
182
|
-
amount: 5000,
|
|
183
205
|
customer_id: "customer-id",
|
|
184
|
-
wallet_id: "wallet-id"
|
|
206
|
+
wallet_id: "wallet-id",
|
|
207
|
+
amount: 5000,
|
|
208
|
+
currency: "NGN",
|
|
209
|
+
method: "card"
|
|
185
210
|
});
|
|
186
211
|
|
|
187
212
|
console.log('Payment URL:', collection.data.url);
|
|
188
213
|
```
|
|
189
214
|
|
|
215
|
+
#### Accept Interac Money Request (CAD)
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// With security answer (standard transfer)
|
|
219
|
+
const interac = await blaaiz.collections.acceptInteracMoneyRequest({
|
|
220
|
+
reference_number: "interac-reference",
|
|
221
|
+
security_answer: "answer",
|
|
222
|
+
email: "sender@example.com" // Optional
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// Auto deposit (no security answer required)
|
|
226
|
+
const interacAutoDeposit = await blaaiz.collections.acceptInteracMoneyRequest({
|
|
227
|
+
reference_number: "interac-reference"
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log('Message:', interac.data.message);
|
|
231
|
+
```
|
|
232
|
+
|
|
190
233
|
#### Crypto Collection
|
|
191
234
|
|
|
192
235
|
```javascript
|
|
@@ -214,24 +257,56 @@ const attachment = await blaaiz.collections.attachCustomer({
|
|
|
214
257
|
|
|
215
258
|
### Payouts
|
|
216
259
|
|
|
217
|
-
#### Bank Transfer Payout
|
|
260
|
+
#### Bank Transfer Payout (NGN)
|
|
218
261
|
|
|
219
262
|
```javascript
|
|
220
263
|
const payout = await blaaiz.payouts.initiate({
|
|
221
264
|
wallet_id: "wallet-id",
|
|
222
265
|
customer_id: "customer-id",
|
|
223
266
|
method: "bank_transfer",
|
|
224
|
-
from_amount: 1000,
|
|
225
|
-
from_currency_id: "
|
|
226
|
-
to_currency_id: "
|
|
267
|
+
from_amount: 1000, // Use from_amount OR to_amount
|
|
268
|
+
from_currency_id: "NGN",
|
|
269
|
+
to_currency_id: "NGN",
|
|
270
|
+
bank_id: "bank-id", // Required for NGN
|
|
227
271
|
account_number: "0123456789",
|
|
228
|
-
|
|
229
|
-
phone_number: "+2348012345678"
|
|
272
|
+
phone_number: "+2348012345678" // Optional
|
|
230
273
|
});
|
|
231
274
|
|
|
232
275
|
console.log('Payout Status:', payout.data.transaction.status);
|
|
233
276
|
```
|
|
234
277
|
|
|
278
|
+
#### Bank Transfer Payout (GBP)
|
|
279
|
+
|
|
280
|
+
```javascript
|
|
281
|
+
const gbpPayout = await blaaiz.payouts.initiate({
|
|
282
|
+
wallet_id: "wallet-id",
|
|
283
|
+
customer_id: "customer-id",
|
|
284
|
+
method: "bank_transfer",
|
|
285
|
+
from_amount: 100,
|
|
286
|
+
from_currency_id: "GBP",
|
|
287
|
+
to_currency_id: "GBP",
|
|
288
|
+
sort_code: "123456",
|
|
289
|
+
account_number: "12345678",
|
|
290
|
+
account_name: "John Doe"
|
|
291
|
+
});
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
#### Bank Transfer Payout (EUR)
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
const eurPayout = await blaaiz.payouts.initiate({
|
|
298
|
+
wallet_id: "wallet-id",
|
|
299
|
+
customer_id: "customer-id",
|
|
300
|
+
method: "bank_transfer",
|
|
301
|
+
from_amount: 100,
|
|
302
|
+
from_currency_id: "EUR",
|
|
303
|
+
to_currency_id: "EUR",
|
|
304
|
+
iban: "DE89370400440532013000",
|
|
305
|
+
bic_code: "COBADEFFXXX",
|
|
306
|
+
account_name: "John Doe"
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
235
310
|
#### Interac Payout (CAD)
|
|
236
311
|
|
|
237
312
|
```javascript
|
|
@@ -240,14 +315,86 @@ const interacPayout = await blaaiz.payouts.initiate({
|
|
|
240
315
|
customer_id: "customer-id",
|
|
241
316
|
method: "interac",
|
|
242
317
|
from_amount: 100,
|
|
243
|
-
from_currency_id: "
|
|
244
|
-
to_currency_id: "
|
|
318
|
+
from_currency_id: "CAD",
|
|
319
|
+
to_currency_id: "CAD",
|
|
245
320
|
email: "recipient@example.com",
|
|
246
321
|
interac_first_name: "John",
|
|
247
322
|
interac_last_name: "Doe"
|
|
248
323
|
});
|
|
249
324
|
```
|
|
250
325
|
|
|
326
|
+
#### ACH Payout (USD)
|
|
327
|
+
|
|
328
|
+
```javascript
|
|
329
|
+
const achPayout = await blaaiz.payouts.initiate({
|
|
330
|
+
wallet_id: "wallet-id",
|
|
331
|
+
customer_id: "customer-id",
|
|
332
|
+
method: "ach",
|
|
333
|
+
from_amount: 100,
|
|
334
|
+
from_currency_id: "USD",
|
|
335
|
+
to_currency_id: "USD",
|
|
336
|
+
type: "individual", // individual or business
|
|
337
|
+
account_number: "123456789",
|
|
338
|
+
account_name: "John Doe",
|
|
339
|
+
account_type: "checking", // checking or savings
|
|
340
|
+
bank_name: "Chase Bank",
|
|
341
|
+
routing_number: "021000021"
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
#### Wire Payout (USD)
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
const wirePayout = await blaaiz.payouts.initiate({
|
|
349
|
+
wallet_id: "wallet-id",
|
|
350
|
+
customer_id: "customer-id",
|
|
351
|
+
method: "wire",
|
|
352
|
+
from_amount: 1000,
|
|
353
|
+
from_currency_id: "USD",
|
|
354
|
+
to_currency_id: "USD",
|
|
355
|
+
type: "individual",
|
|
356
|
+
account_number: "123456789",
|
|
357
|
+
account_name: "John Doe",
|
|
358
|
+
account_type: "checking",
|
|
359
|
+
bank_name: "Chase Bank",
|
|
360
|
+
routing_number: "021000021",
|
|
361
|
+
swift_code: "CHASUS33"
|
|
362
|
+
});
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
#### Crypto Payout
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
const cryptoPayout = await blaaiz.payouts.initiate({
|
|
369
|
+
wallet_id: "wallet-id",
|
|
370
|
+
customer_id: "customer-id",
|
|
371
|
+
method: "crypto",
|
|
372
|
+
from_amount: 100,
|
|
373
|
+
from_currency_id: "USD",
|
|
374
|
+
to_currency_id: "USDT",
|
|
375
|
+
wallet_address: "0x1234567890abcdef...",
|
|
376
|
+
wallet_token: "USDT", // USDT or USDC
|
|
377
|
+
wallet_network: "ETHEREUM_MAINNET" // BSC_MAINNET, ETHEREUM_MAINNET, TRON_MAINNET, MATIC_MAINNET
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
#### Using to_amount Instead of from_amount
|
|
382
|
+
|
|
383
|
+
You can specify the exact amount the recipient should receive:
|
|
384
|
+
|
|
385
|
+
```javascript
|
|
386
|
+
const payout = await blaaiz.payouts.initiate({
|
|
387
|
+
wallet_id: "wallet-id",
|
|
388
|
+
customer_id: "customer-id",
|
|
389
|
+
method: "bank_transfer",
|
|
390
|
+
to_amount: 50000, // Recipient gets exactly this amount
|
|
391
|
+
from_currency_id: "USD",
|
|
392
|
+
to_currency_id: "NGN",
|
|
393
|
+
bank_id: "bank-id",
|
|
394
|
+
account_number: "0123456789"
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
251
398
|
### Virtual Bank Accounts
|
|
252
399
|
|
|
253
400
|
#### Create Virtual Bank Account
|
|
@@ -264,11 +411,51 @@ console.log('Bank Name:', vba.data.bank_name);
|
|
|
264
411
|
|
|
265
412
|
#### List Virtual Bank Accounts
|
|
266
413
|
|
|
414
|
+
You can optionally filter by `wallet_id`, `customer_id`, or both.
|
|
415
|
+
|
|
267
416
|
```javascript
|
|
268
|
-
|
|
417
|
+
// All virtual bank accounts
|
|
418
|
+
const vbas = await blaaiz.virtualBankAccounts.list();
|
|
419
|
+
|
|
420
|
+
// Filter by wallet
|
|
421
|
+
const vbasByWallet = await blaaiz.virtualBankAccounts.list("wallet-id");
|
|
422
|
+
|
|
423
|
+
// Filter by customer
|
|
424
|
+
const vbasByCustomer = await blaaiz.virtualBankAccounts.list(null, "customer-id");
|
|
425
|
+
|
|
426
|
+
// Filter by both wallet and customer
|
|
427
|
+
const vbasByBoth = await blaaiz.virtualBankAccounts.list("wallet-id", "customer-id");
|
|
428
|
+
|
|
269
429
|
console.log('Virtual Accounts:', vbas.data);
|
|
270
430
|
```
|
|
271
431
|
|
|
432
|
+
#### Close Virtual Bank Account
|
|
433
|
+
|
|
434
|
+
```javascript
|
|
435
|
+
// Close without reason
|
|
436
|
+
const closed = await blaaiz.virtualBankAccounts.close("vba-id");
|
|
437
|
+
|
|
438
|
+
// Close with reason
|
|
439
|
+
const closedWithReason = await blaaiz.virtualBankAccounts.close("vba-id", "No longer needed");
|
|
440
|
+
|
|
441
|
+
console.log('Status:', closed.data.status);
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
#### Get Identification Type
|
|
445
|
+
|
|
446
|
+
Retrieve the expected identification type label based on country and customer type. This is useful for determining what identification documents are required for a customer.
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
// Using customer_id (recommended if customer already exists)
|
|
450
|
+
const idType = await blaaiz.virtualBankAccounts.getIdentificationType("customer-id");
|
|
451
|
+
console.log('Required ID:', idType.data.label); // e.g., "Bank Verification Number"
|
|
452
|
+
console.log('Type:', idType.data.type); // e.g., "bvn"
|
|
453
|
+
|
|
454
|
+
// Using country and type (for new customers)
|
|
455
|
+
const idTypeNew = await blaaiz.virtualBankAccounts.getIdentificationType(null, "NG", "individual");
|
|
456
|
+
console.log('Required ID:', idTypeNew.data.label);
|
|
457
|
+
```
|
|
458
|
+
|
|
272
459
|
### Wallets
|
|
273
460
|
|
|
274
461
|
#### List All Wallets
|
|
@@ -338,15 +525,25 @@ console.log('Supported Currencies:', currencies.data);
|
|
|
338
525
|
#### Get Fee Breakdown
|
|
339
526
|
|
|
340
527
|
```javascript
|
|
528
|
+
// Using from_amount (calculate what recipient gets)
|
|
341
529
|
const feeBreakdown = await blaaiz.fees.getBreakdown({
|
|
342
|
-
from_currency_id: "
|
|
343
|
-
to_currency_id: "
|
|
530
|
+
from_currency_id: "NGN",
|
|
531
|
+
to_currency_id: "CAD",
|
|
344
532
|
from_amount: 100000
|
|
345
533
|
});
|
|
346
534
|
|
|
347
535
|
console.log('You send:', feeBreakdown.data.you_send);
|
|
348
536
|
console.log('Recipient gets:', feeBreakdown.data.recipient_gets);
|
|
349
537
|
console.log('Total fees:', feeBreakdown.data.total_fees);
|
|
538
|
+
|
|
539
|
+
// Using to_amount (calculate what you need to send)
|
|
540
|
+
const feeBreakdownReverse = await blaaiz.fees.getBreakdown({
|
|
541
|
+
from_currency_id: "USD",
|
|
542
|
+
to_currency_id: "NGN",
|
|
543
|
+
to_amount: 50000 // Recipient should get exactly this
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
console.log('You send:', feeBreakdownReverse.data.you_send);
|
|
350
547
|
```
|
|
351
548
|
|
|
352
549
|
### Webhooks
|
|
@@ -375,12 +572,23 @@ const replay = await blaaiz.webhooks.replay({
|
|
|
375
572
|
});
|
|
376
573
|
```
|
|
377
574
|
|
|
575
|
+
#### Simulate Interac Webhook (Non-Production Only)
|
|
576
|
+
|
|
577
|
+
```javascript
|
|
578
|
+
// For testing Interac webhooks in development/sandbox environment
|
|
579
|
+
const simulate = await blaaiz.webhooks.simulateInteracWebhook({
|
|
580
|
+
interac_email: "sender@example.com"
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
console.log('Message:', simulate.message);
|
|
584
|
+
```
|
|
585
|
+
|
|
378
586
|
## Advanced Usage
|
|
379
587
|
|
|
380
588
|
### Complete Payout Workflow
|
|
381
589
|
|
|
382
590
|
```javascript
|
|
383
|
-
const completePayoutResult = await blaaiz.
|
|
591
|
+
const completePayoutResult = await blaaiz.createCompletePayout({
|
|
384
592
|
customerData: {
|
|
385
593
|
first_name: "John",
|
|
386
594
|
last_name: "Doe",
|
|
@@ -394,10 +602,10 @@ const completePayoutResult = await blaaiz.createCompleteePayout({
|
|
|
394
602
|
wallet_id: "wallet-id",
|
|
395
603
|
method: "bank_transfer",
|
|
396
604
|
from_amount: 1000,
|
|
397
|
-
from_currency_id: "
|
|
398
|
-
to_currency_id: "
|
|
605
|
+
from_currency_id: "NGN",
|
|
606
|
+
to_currency_id: "NGN",
|
|
399
607
|
account_number: "0123456789",
|
|
400
|
-
bank_id: "
|
|
608
|
+
bank_id: "bank-id",
|
|
401
609
|
phone_number: "+2348012345678"
|
|
402
610
|
}
|
|
403
611
|
});
|
|
@@ -423,6 +631,7 @@ const completeCollectionResult = await blaaiz.createCompleteCollection({
|
|
|
423
631
|
collectionData: {
|
|
424
632
|
method: "card",
|
|
425
633
|
amount: 5000,
|
|
634
|
+
currency: "NGN",
|
|
426
635
|
wallet_id: "wallet-id"
|
|
427
636
|
},
|
|
428
637
|
createVBA: true // Optionally create a virtual bank account
|
|
@@ -463,7 +672,7 @@ The Blaaiz API has a rate limit of 100 requests per minute. The SDK automaticall
|
|
|
463
672
|
|
|
464
673
|
### Webhook Signature Verification
|
|
465
674
|
|
|
466
|
-
The SDK provides built-in webhook signature verification to ensure webhook authenticity
|
|
675
|
+
The SDK provides built-in webhook signature verification to ensure webhook authenticity. The signature is computed using HMAC-SHA256 with the format `timestamp.payload`.
|
|
467
676
|
|
|
468
677
|
```javascript
|
|
469
678
|
const { Blaaiz } = require('blaaiz-nodejs-sdk');
|
|
@@ -473,7 +682,8 @@ const blaaiz = new Blaaiz('your-api-key');
|
|
|
473
682
|
// Method 1: Verify signature manually
|
|
474
683
|
const isValid = blaaiz.webhooks.verifySignature(
|
|
475
684
|
payload, // Raw webhook payload (string or object)
|
|
476
|
-
signature, // Signature from webhook headers
|
|
685
|
+
signature, // Signature from webhook headers (x-blaaiz-signature)
|
|
686
|
+
timestamp, // Timestamp from webhook headers (x-blaaiz-timestamp)
|
|
477
687
|
webhookSecret // Your webhook secret key
|
|
478
688
|
);
|
|
479
689
|
|
|
@@ -487,10 +697,11 @@ if (isValid) {
|
|
|
487
697
|
try {
|
|
488
698
|
const event = blaaiz.webhooks.constructEvent(
|
|
489
699
|
payload, // Raw webhook payload
|
|
490
|
-
signature, // Signature from webhook headers
|
|
700
|
+
signature, // Signature from webhook headers
|
|
701
|
+
timestamp, // Timestamp from webhook headers
|
|
491
702
|
webhookSecret // Your webhook secret key
|
|
492
703
|
);
|
|
493
|
-
|
|
704
|
+
|
|
494
705
|
console.log('Verified event:', event);
|
|
495
706
|
// event.verified will be true
|
|
496
707
|
// event.timestamp will contain verification timestamp
|
|
@@ -517,12 +728,13 @@ app.use('/webhooks', express.raw({ type: 'application/json' }));
|
|
|
517
728
|
// Collection webhook with signature verification
|
|
518
729
|
app.post('/webhooks/collection', (req, res) => {
|
|
519
730
|
const signature = req.headers['x-blaaiz-signature'];
|
|
731
|
+
const timestamp = req.headers['x-blaaiz-timestamp'];
|
|
520
732
|
const payload = req.body.toString();
|
|
521
|
-
|
|
733
|
+
|
|
522
734
|
try {
|
|
523
735
|
// Verify webhook signature and construct event
|
|
524
|
-
const event = blaaiz.webhooks.constructEvent(payload, signature, WEBHOOK_SECRET);
|
|
525
|
-
|
|
736
|
+
const event = blaaiz.webhooks.constructEvent(payload, signature, timestamp, WEBHOOK_SECRET);
|
|
737
|
+
|
|
526
738
|
console.log('Verified collection event:', {
|
|
527
739
|
transaction_id: event.transaction_id,
|
|
528
740
|
status: event.status,
|
|
@@ -530,10 +742,10 @@ app.post('/webhooks/collection', (req, res) => {
|
|
|
530
742
|
currency: event.currency,
|
|
531
743
|
verified: event.verified
|
|
532
744
|
});
|
|
533
|
-
|
|
745
|
+
|
|
534
746
|
// Process the collection
|
|
535
747
|
// Update your database, send notifications, etc.
|
|
536
|
-
|
|
748
|
+
|
|
537
749
|
res.status(200).json({ received: true });
|
|
538
750
|
} catch (error) {
|
|
539
751
|
console.error('Webhook verification failed:', error.message);
|
|
@@ -544,22 +756,23 @@ app.post('/webhooks/collection', (req, res) => {
|
|
|
544
756
|
// Payout webhook with signature verification
|
|
545
757
|
app.post('/webhooks/payout', (req, res) => {
|
|
546
758
|
const signature = req.headers['x-blaaiz-signature'];
|
|
759
|
+
const timestamp = req.headers['x-blaaiz-timestamp'];
|
|
547
760
|
const payload = req.body.toString();
|
|
548
|
-
|
|
761
|
+
|
|
549
762
|
try {
|
|
550
763
|
// Verify webhook signature and construct event
|
|
551
|
-
const event = blaaiz.webhooks.constructEvent(payload, signature, WEBHOOK_SECRET);
|
|
552
|
-
|
|
764
|
+
const event = blaaiz.webhooks.constructEvent(payload, signature, timestamp, WEBHOOK_SECRET);
|
|
765
|
+
|
|
553
766
|
console.log('Verified payout event:', {
|
|
554
767
|
transaction_id: event.transaction_id,
|
|
555
768
|
status: event.status,
|
|
556
769
|
recipient: event.recipient,
|
|
557
770
|
verified: event.verified
|
|
558
771
|
});
|
|
559
|
-
|
|
772
|
+
|
|
560
773
|
// Process the payout completion
|
|
561
774
|
// Update your database, send notifications, etc.
|
|
562
|
-
|
|
775
|
+
|
|
563
776
|
res.status(200).json({ received: true });
|
|
564
777
|
} catch (error) {
|
|
565
778
|
console.error('Webhook verification failed:', error.message);
|
|
@@ -579,20 +792,21 @@ If you prefer manual verification:
|
|
|
579
792
|
```javascript
|
|
580
793
|
app.post('/webhooks/collection', (req, res) => {
|
|
581
794
|
const signature = req.headers['x-blaaiz-signature'];
|
|
795
|
+
const timestamp = req.headers['x-blaaiz-timestamp'];
|
|
582
796
|
const payload = req.body.toString();
|
|
583
|
-
|
|
797
|
+
|
|
584
798
|
// Verify signature manually
|
|
585
|
-
const isValid = blaaiz.webhooks.verifySignature(payload, signature, WEBHOOK_SECRET);
|
|
586
|
-
|
|
799
|
+
const isValid = blaaiz.webhooks.verifySignature(payload, signature, timestamp, WEBHOOK_SECRET);
|
|
800
|
+
|
|
587
801
|
if (!isValid) {
|
|
588
802
|
console.error('Invalid webhook signature');
|
|
589
803
|
return res.status(400).json({ error: 'Invalid signature' });
|
|
590
804
|
}
|
|
591
|
-
|
|
805
|
+
|
|
592
806
|
// Parse payload manually
|
|
593
807
|
const event = JSON.parse(payload);
|
|
594
808
|
console.log('Collection received:', event);
|
|
595
|
-
|
|
809
|
+
|
|
596
810
|
res.status(200).json({ received: true });
|
|
597
811
|
});
|
|
598
812
|
```
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@ class CollectionService {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async initiate (collectionData) {
|
|
7
|
-
const requiredFields = ['
|
|
7
|
+
const requiredFields = ['customer_id', 'wallet_id', 'amount', 'currency', 'method']
|
|
8
8
|
for (const field of requiredFields) {
|
|
9
9
|
if (!collectionData[field]) {
|
|
10
10
|
throw new Error(`${field} is required`)
|
|
@@ -32,6 +32,14 @@ class CollectionService {
|
|
|
32
32
|
async getCryptoNetworks () {
|
|
33
33
|
return this.client.makeRequest('GET', '/api/external/collection/crypto/networks')
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
async acceptInteracMoneyRequest (interacData) {
|
|
37
|
+
if (!interacData || !interacData.reference_number) {
|
|
38
|
+
throw new Error('reference_number is required')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return this.client.makeRequest('POST', '/api/external/collection/accept-interac-money-request', interacData)
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
module.exports = CollectionService
|
|
@@ -4,14 +4,21 @@ class CustomerService {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async create (customerData) {
|
|
7
|
-
const requiredFields = ['
|
|
7
|
+
const requiredFields = ['type', 'email', 'country', 'id_type', 'id_number']
|
|
8
8
|
for (const field of requiredFields) {
|
|
9
9
|
if (!customerData[field]) {
|
|
10
10
|
throw new Error(`${field} is required`)
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
if (customerData.type === '
|
|
14
|
+
if (customerData.type === 'individual') {
|
|
15
|
+
if (!customerData.first_name) {
|
|
16
|
+
throw new Error('first_name is required when type is individual')
|
|
17
|
+
}
|
|
18
|
+
if (!customerData.last_name) {
|
|
19
|
+
throw new Error('last_name is required when type is individual')
|
|
20
|
+
}
|
|
21
|
+
} else if (customerData.type === 'business' && !customerData.business_name) {
|
|
15
22
|
throw new Error('business_name is required when type is business')
|
|
16
23
|
}
|
|
17
24
|
|
|
@@ -50,6 +57,23 @@ class CustomerService {
|
|
|
50
57
|
return this.client.makeRequest('PUT', `/api/external/customer/${customerId}/files`, fileData)
|
|
51
58
|
}
|
|
52
59
|
|
|
60
|
+
async listBeneficiaries (customerId) {
|
|
61
|
+
if (!customerId) {
|
|
62
|
+
throw new Error('Customer ID is required')
|
|
63
|
+
}
|
|
64
|
+
return this.client.makeRequest('GET', `/api/external/customer/${customerId}/beneficiary`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async getBeneficiary (customerId, beneficiaryId) {
|
|
68
|
+
if (!customerId) {
|
|
69
|
+
throw new Error('Customer ID is required')
|
|
70
|
+
}
|
|
71
|
+
if (!beneficiaryId) {
|
|
72
|
+
throw new Error('Beneficiary ID is required')
|
|
73
|
+
}
|
|
74
|
+
return this.client.makeRequest('GET', `/api/external/customer/${customerId}/beneficiary/${beneficiaryId}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
53
77
|
async uploadFileComplete (customerId, fileOptions) {
|
|
54
78
|
if (!customerId) {
|
|
55
79
|
throw new Error('Customer ID is required')
|
|
@@ -4,13 +4,18 @@ class FeesService {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async getBreakdown (feeData) {
|
|
7
|
-
const requiredFields = ['from_currency_id', 'to_currency_id'
|
|
7
|
+
const requiredFields = ['from_currency_id', 'to_currency_id']
|
|
8
8
|
for (const field of requiredFields) {
|
|
9
9
|
if (!feeData[field]) {
|
|
10
10
|
throw new Error(`${field} is required`)
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Either from_amount or to_amount must be provided
|
|
15
|
+
if (!feeData.from_amount && !feeData.to_amount) {
|
|
16
|
+
throw new Error('Either from_amount or to_amount is required')
|
|
17
|
+
}
|
|
18
|
+
|
|
14
19
|
return this.client.makeRequest('POST', '/api/external/fees/breakdown', feeData)
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -4,28 +4,64 @@ class PayoutService {
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
async initiate (payoutData) {
|
|
7
|
-
const requiredFields = ['wallet_id', '
|
|
7
|
+
const requiredFields = ['wallet_id', 'customer_id', 'method', 'from_currency_id', 'to_currency_id']
|
|
8
8
|
for (const field of requiredFields) {
|
|
9
9
|
if (!payoutData[field]) {
|
|
10
10
|
throw new Error(`${field} is required`)
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Either from_amount or to_amount must be provided
|
|
15
|
+
if (!payoutData.from_amount && !payoutData.to_amount) {
|
|
16
|
+
throw new Error('Either from_amount or to_amount is required')
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
const method = payoutData.method
|
|
20
|
+
const toCurrency = payoutData.to_currency_id
|
|
21
|
+
|
|
22
|
+
// Method-specific validations
|
|
23
|
+
if (method === 'bank_transfer') {
|
|
24
|
+
this._validateBankTransferFields(payoutData, toCurrency)
|
|
25
|
+
} else if (method === 'interac') {
|
|
26
|
+
this._validateRequiredFields(payoutData, ['email', 'interac_first_name', 'interac_last_name'], 'interac')
|
|
27
|
+
} else if (method === 'ach' || method === 'wire') {
|
|
28
|
+
this._validateAchWireFields(payoutData, method)
|
|
29
|
+
} else if (method === 'crypto') {
|
|
30
|
+
this._validateRequiredFields(payoutData, ['wallet_address', 'wallet_token', 'wallet_network'], 'crypto')
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
return this.client.makeRequest('POST', '/api/external/payout', payoutData)
|
|
28
34
|
}
|
|
35
|
+
|
|
36
|
+
_validateRequiredFields (data, fields, methodName) {
|
|
37
|
+
for (const field of fields) {
|
|
38
|
+
if (!data[field]) {
|
|
39
|
+
throw new Error(`${field} is required for ${methodName} method`)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_validateBankTransferFields (payoutData, toCurrency) {
|
|
45
|
+
// NGN bank transfers require bank_id and account_number
|
|
46
|
+
if (toCurrency === 'NGN') {
|
|
47
|
+
this._validateRequiredFields(payoutData, ['bank_id', 'account_number'], 'NGN bank_transfer')
|
|
48
|
+
} else if (toCurrency === 'GBP') {
|
|
49
|
+
// GBP bank transfers require sort_code and account_number
|
|
50
|
+
this._validateRequiredFields(payoutData, ['sort_code', 'account_number', 'account_name'], 'GBP bank_transfer')
|
|
51
|
+
} else if (toCurrency === 'EUR') {
|
|
52
|
+
// EUR bank transfers require IBAN and BIC code
|
|
53
|
+
this._validateRequiredFields(payoutData, ['iban', 'bic_code', 'account_name'], 'EUR bank_transfer')
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
_validateAchWireFields (payoutData, method) {
|
|
58
|
+
const baseFields = ['type', 'account_number', 'account_name', 'account_type', 'bank_name', 'routing_number']
|
|
59
|
+
this._validateRequiredFields(payoutData, baseFields, method)
|
|
60
|
+
|
|
61
|
+
if (method === 'wire') {
|
|
62
|
+
this._validateRequiredFields(payoutData, ['swift_code'], 'wire')
|
|
63
|
+
}
|
|
64
|
+
}
|
|
29
65
|
}
|
|
30
66
|
|
|
31
67
|
module.exports = PayoutService
|
|
@@ -14,9 +14,22 @@ class VirtualBankAccountService {
|
|
|
14
14
|
return this.client.makeRequest('POST', '/api/external/virtual-bank-account', vbaData)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
async list (walletId) {
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
async list (walletId = null, customerId = null) {
|
|
18
|
+
let endpoint = '/api/external/virtual-bank-account'
|
|
19
|
+
const params = []
|
|
20
|
+
|
|
21
|
+
if (walletId) {
|
|
22
|
+
params.push(`wallet_id=${encodeURIComponent(walletId)}`)
|
|
23
|
+
}
|
|
24
|
+
if (customerId) {
|
|
25
|
+
params.push(`customer_id=${encodeURIComponent(customerId)}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (params.length > 0) {
|
|
29
|
+
endpoint += '?' + params.join('&')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.client.makeRequest('GET', endpoint)
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
async get (vbaId) {
|
|
@@ -25,6 +38,39 @@ class VirtualBankAccountService {
|
|
|
25
38
|
}
|
|
26
39
|
return this.client.makeRequest('GET', `/api/external/virtual-bank-account/${vbaId}`)
|
|
27
40
|
}
|
|
41
|
+
|
|
42
|
+
async close (vbaId, reason = null) {
|
|
43
|
+
if (!vbaId) {
|
|
44
|
+
throw new Error('Virtual bank account ID is required')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = {}
|
|
48
|
+
if (reason !== null) {
|
|
49
|
+
data.reason = reason
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this.client.makeRequest('POST', `/api/external/virtual-bank-account/${vbaId}/close`, data)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async getIdentificationType (customerId = null, country = null, type = null) {
|
|
56
|
+
if (!customerId && (!country || !type)) {
|
|
57
|
+
throw new Error('Either customer_id or both country and type are required')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let endpoint = '/api/external/virtual-bank-account/identification-type'
|
|
61
|
+
const params = []
|
|
62
|
+
|
|
63
|
+
if (customerId) {
|
|
64
|
+
params.push(`customer_id=${encodeURIComponent(customerId)}`)
|
|
65
|
+
} else {
|
|
66
|
+
params.push(`country=${encodeURIComponent(country)}`)
|
|
67
|
+
params.push(`type=${encodeURIComponent(type)}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
endpoint += '?' + params.join('&')
|
|
71
|
+
|
|
72
|
+
return this.client.makeRequest('GET', endpoint)
|
|
73
|
+
}
|
|
28
74
|
}
|
|
29
75
|
|
|
30
76
|
module.exports = VirtualBankAccountService
|
|
@@ -33,7 +33,11 @@ class WebhookService {
|
|
|
33
33
|
return this.client.makeRequest('POST', '/api/external/webhook/replay', replayData)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
async simulateInteracWebhook (simulateData) {
|
|
37
|
+
return this.client.makeRequest('POST', '/api/external/mock/simulate-webhook/interac', simulateData)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
verifySignature (payload, signature, timestamp, secret) {
|
|
37
41
|
if (!payload) {
|
|
38
42
|
throw new Error('Payload is required for signature verification')
|
|
39
43
|
}
|
|
@@ -42,6 +46,10 @@ class WebhookService {
|
|
|
42
46
|
throw new Error('Signature is required for signature verification')
|
|
43
47
|
}
|
|
44
48
|
|
|
49
|
+
if (!timestamp) {
|
|
50
|
+
throw new Error('Timestamp is required for signature verification')
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
if (!secret) {
|
|
46
54
|
throw new Error('Webhook secret is required for signature verification')
|
|
47
55
|
}
|
|
@@ -51,18 +59,18 @@ class WebhookService {
|
|
|
51
59
|
// Convert payload to string if it's an object
|
|
52
60
|
const payloadString = typeof payload === 'string' ? payload : JSON.stringify(payload)
|
|
53
61
|
|
|
54
|
-
//
|
|
55
|
-
const
|
|
62
|
+
// Create signed payload with timestamp
|
|
63
|
+
const signedPayload = `${timestamp}.${payloadString}`
|
|
56
64
|
|
|
57
65
|
// Create HMAC signature
|
|
58
66
|
const expectedSignature = crypto
|
|
59
67
|
.createHmac('sha256', secret)
|
|
60
|
-
.update(
|
|
68
|
+
.update(signedPayload, 'utf8')
|
|
61
69
|
.digest('hex')
|
|
62
70
|
|
|
63
71
|
// Use timingSafeEqual to prevent timing attacks
|
|
64
72
|
try {
|
|
65
|
-
const signatureBuffer = Buffer.from(
|
|
73
|
+
const signatureBuffer = Buffer.from(signature.toLowerCase(), 'hex')
|
|
66
74
|
const expectedBuffer = Buffer.from(expectedSignature, 'hex')
|
|
67
75
|
|
|
68
76
|
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
@@ -75,8 +83,8 @@ class WebhookService {
|
|
|
75
83
|
}
|
|
76
84
|
}
|
|
77
85
|
|
|
78
|
-
constructEvent (payload, signature, secret) {
|
|
79
|
-
if (!this.verifySignature(payload, signature, secret)) {
|
|
86
|
+
constructEvent (payload, signature, timestamp, secret) {
|
|
87
|
+
if (!this.verifySignature(payload, signature, timestamp, secret)) {
|
|
80
88
|
throw new Error('Invalid webhook signature')
|
|
81
89
|
}
|
|
82
90
|
|