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 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 Interac payouts across multiple currencies
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**: All supported currencies
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
- phone: "+1234567890" // Optional
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: "1", // NGN
226
- to_currency_id: "1", // NGN
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
- bank_id: "1", // Required for NGN
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: "2", // CAD
244
- to_currency_id: "2", // CAD
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
- const vbas = await blaaiz.virtualBankAccounts.list("wallet-id");
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: "1", // NGN
343
- to_currency_id: "2", // CAD
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.createCompleteePayout({
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: "1",
398
- to_currency_id: "1",
605
+ from_currency_id: "NGN",
606
+ to_currency_id: "NGN",
399
607
  account_number: "0123456789",
400
- bank_id: "1",
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blaaiz-nodejs-sdk",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "Official Node.js SDK for Blaaiz RaaS (Remittance as a Service) API",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -4,7 +4,7 @@ class CollectionService {
4
4
  }
5
5
 
6
6
  async initiate (collectionData) {
7
- const requiredFields = ['method', 'amount', 'wallet_id']
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 = ['first_name', 'last_name', 'type', 'email', 'country', 'id_type', 'id_number']
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 === 'business' && !customerData.business_name) {
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', 'from_amount']
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', 'method', 'from_amount', 'from_currency_id', 'to_currency_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
- if (payoutData.method === 'bank_transfer' && !payoutData.account_number) {
15
- throw new Error('account_number is required for bank_transfer method')
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
- if (payoutData.method === 'interac') {
19
- const interacFields = ['email', 'interac_first_name', 'interac_last_name']
20
- for (const field of interacFields) {
21
- if (!payoutData[field]) {
22
- throw new Error(`${field} is required for interac method`)
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
- const params = walletId ? `?wallet_id=${walletId}` : ''
19
- return this.client.makeRequest('GET', `/api/external/virtual-bank-account${params}`)
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
- verifySignature (payload, signature, secret) {
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
- // Remove 'sha256=' prefix if present
55
- const cleanSignature = signature.replace(/^sha256=/, '')
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(payloadString, 'utf8')
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(cleanSignature, 'hex')
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