nowpayments 0.2.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.
data/docs/API.md ADDED
@@ -0,0 +1,922 @@
1
+ # NOWPayments Ruby SDK - API Reference
2
+
3
+ Complete API documentation with all available methods and usage examples.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Authentication](#authentication)
8
+ - [JWT Authentication](#jwt-authentication)
9
+ - [Status API](#status-api)
10
+ - [Payments API](#payments-api)
11
+ - [Invoices API](#invoices-api)
12
+ - [Estimates API](#estimates-api)
13
+ - [Recurring Payments API](#recurring-payments-api)
14
+ - [Mass Payouts API](#mass-payouts-api)
15
+ - [Conversions API](#conversions-api)
16
+ - [Custody API](#custody-api)
17
+ - [Error Handling](#error-handling)
18
+
19
+ ---
20
+
21
+ ## Authentication
22
+
23
+ ### Initialize Client
24
+
25
+ ```ruby
26
+ require 'nowpayments'
27
+
28
+ # Production
29
+ client = NOWPayments::Client.new(
30
+ api_key: ENV['NOWPAYMENTS_API_KEY'],
31
+ ipn_secret: ENV['NOWPAYMENTS_IPN_SECRET'] # Optional, for webhook verification
32
+ )
33
+
34
+ # Sandbox (for testing)
35
+ client = NOWPayments::Client.new(
36
+ api_key: ENV['NOWPAYMENTS_SANDBOX_API_KEY'],
37
+ ipn_secret: ENV['NOWPAYMENTS_SANDBOX_IPN_SECRET'],
38
+ sandbox: true
39
+ )
40
+ ```
41
+
42
+ ### JWT Authentication
43
+
44
+ Some endpoints require JWT authentication (token expires after 5 minutes):
45
+
46
+ **Required for:**
47
+ - Mass Payouts (create, verify, list)
48
+ - Conversions (all endpoints)
49
+ - Custody Operations (transfers, write-offs)
50
+ - Recurring Payments (delete)
51
+
52
+ #### `authenticate(email:, password:)`
53
+
54
+ Authenticate and obtain a JWT token (valid for 5 minutes).
55
+
56
+ ```ruby
57
+ response = client.authenticate(
58
+ email: 'your_email@example.com',
59
+ password: 'your_password'
60
+ )
61
+ # => {"token" => "eyJhbGc..."}
62
+
63
+ # Token is automatically stored and injected in subsequent requests
64
+ ```
65
+
66
+ #### `jwt_token(email:, password:)`
67
+
68
+ Get current JWT token, optionally refreshing if expired.
69
+
70
+ ```ruby
71
+ # Get current token without refresh
72
+ token = client.jwt_token
73
+ # => "eyJhbGc..." or nil if expired/not authenticated
74
+
75
+ # Get token with auto-refresh if expired
76
+ token = client.jwt_token(
77
+ email: 'your_email@example.com',
78
+ password: 'your_password'
79
+ )
80
+ # => "eyJhbGc..."
81
+ ```
82
+
83
+ #### `jwt_expired?`
84
+
85
+ Check if JWT token is expired or missing.
86
+
87
+ ```ruby
88
+ client.jwt_expired? # => false
89
+ ```
90
+
91
+ #### `jwt_time_remaining`
92
+
93
+ Get seconds until JWT token expires.
94
+
95
+ ```ruby
96
+ client.jwt_time_remaining # => 287
97
+ ```
98
+
99
+ #### `clear_jwt_token`
100
+
101
+ Manually clear stored JWT token (optional, for security).
102
+
103
+ ```ruby
104
+ client.clear_jwt_token
105
+ client.jwt_expired? # => true
106
+ ```
107
+
108
+ **Auto-refresh pattern:**
109
+
110
+ ```ruby
111
+ EMAIL = 'your_email@example.com'
112
+ PASSWORD = 'your_password'
113
+
114
+ def ensure_authenticated(client, email, password)
115
+ return unless client.jwt_expired?
116
+ client.authenticate(email: email, password: password)
117
+ end
118
+
119
+ # Before each JWT-required operation
120
+ ensure_authenticated(client, EMAIL, PASSWORD)
121
+ payout = client.create_payout(...)
122
+ ```
123
+
124
+ ---
125
+
126
+ ## Status API
127
+
128
+ ### `status`
129
+
130
+ Get API status (uptime, latency).
131
+
132
+ ```ruby
133
+ status = client.status
134
+ # => {"message" => "OK"}
135
+ ```
136
+
137
+ ### `currencies`
138
+
139
+ Get list of available currencies.
140
+
141
+ ```ruby
142
+ currencies = client.currencies
143
+ # => {"currencies" => ["btc", "eth", "usdt", ...]}
144
+ ```
145
+
146
+ ### `currencies_full`
147
+
148
+ Get detailed currency information including logos, networks, and limits.
149
+
150
+ ```ruby
151
+ currencies = client.currencies_full
152
+ # => {"currencies" => [{"currency" => "btc", "logo_url" => "...", "network" => "BTC", ...}, ...]}
153
+ ```
154
+
155
+ ### `merchant_currencies`
156
+
157
+ Get currencies available to your merchant account.
158
+
159
+ ```ruby
160
+ currencies = client.merchant_currencies
161
+ # => {"currencies" => ["btc", "eth", ...]}
162
+ ```
163
+
164
+ ### `selected_currencies`
165
+
166
+ Get currencies you've enabled in your account settings.
167
+
168
+ ```ruby
169
+ currencies = client.selected_currencies
170
+ # => {"currencies" => ["btc", "eth", "usdt"]}
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Payments API
176
+
177
+ ### `create_payment(price_amount:, price_currency:, pay_currency:, **params)`
178
+
179
+ Create a new payment.
180
+
181
+ ```ruby
182
+ payment = client.create_payment(
183
+ price_amount: 100.0, # Amount in price_currency
184
+ price_currency: 'usd', # Fiat or crypto
185
+ pay_currency: 'btc', # Cryptocurrency customer pays with
186
+ order_id: 'order-123', # Your internal order ID
187
+ order_description: 'Pro Plan - Annual',
188
+ ipn_callback_url: 'https://example.com/webhooks/nowpayments',
189
+ success_url: 'https://example.com/success',
190
+ cancel_url: 'https://example.com/cancel'
191
+ )
192
+ # => {
193
+ # "payment_id" => "5729887098",
194
+ # "payment_status" => "waiting",
195
+ # "pay_address" => "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
196
+ # "pay_amount" => 0.00123456,
197
+ # "pay_currency" => "btc",
198
+ # "price_amount" => 100.0,
199
+ # "price_currency" => "usd",
200
+ # "order_id" => "order-123",
201
+ # "expiration_estimate_date" => "2025-11-01T12:30:00.000Z"
202
+ # }
203
+ ```
204
+
205
+ ### `payment(payment_id)`
206
+
207
+ Get payment status and details.
208
+
209
+ ```ruby
210
+ payment = client.payment('5729887098')
211
+ # => {
212
+ # "payment_id" => "5729887098",
213
+ # "payment_status" => "finished",
214
+ # "pay_address" => "bc1q...",
215
+ # "pay_amount" => 0.00123456,
216
+ # "actually_paid" => 0.00123456,
217
+ # "outcome_amount" => 99.5,
218
+ # "outcome_currency" => "usd"
219
+ # }
220
+ ```
221
+
222
+ **Payment statuses:**
223
+ - `waiting` - Waiting for customer to send cryptocurrency
224
+ - `confirming` - Payment received, waiting for confirmations
225
+ - `confirmed` - Payment confirmed
226
+ - `sending` - Sending to your wallet
227
+ - `partially_paid` - Customer sent wrong amount
228
+ - `finished` - Payment complete
229
+ - `failed` - Payment failed
230
+ - `refunded` - Payment refunded
231
+ - `expired` - Payment expired
232
+
233
+ ### `minimum_payment_amount(currency_from:, currency_to:, fiat_equivalent:)`
234
+
235
+ Get minimum payment amount for a currency pair.
236
+
237
+ ```ruby
238
+ minimum = client.minimum_payment_amount(
239
+ currency_from: 'btc',
240
+ currency_to: 'eth',
241
+ fiat_equivalent: 'usd'
242
+ )
243
+ # => {
244
+ # "currency_from" => "btc",
245
+ # "currency_to" => "eth",
246
+ # "fiat_equivalent" => "usd",
247
+ # "min_amount" => 0.0001
248
+ # }
249
+ ```
250
+
251
+ ### `balance`
252
+
253
+ Get your account balance.
254
+
255
+ ```ruby
256
+ balance = client.balance
257
+ # => {
258
+ # "btc" => 0.5,
259
+ # "eth" => 10.0,
260
+ # "usdt" => 1000.0
261
+ # }
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Invoices API
267
+
268
+ Create hosted payment pages where customers can choose from 150+ cryptocurrencies.
269
+
270
+ ### `create_invoice(price_amount:, price_currency:, order_id:, **params)`
271
+
272
+ Create a payment invoice with hosted page.
273
+
274
+ ```ruby
275
+ invoice = client.create_invoice(
276
+ price_amount: 99.0,
277
+ price_currency: 'usd',
278
+ order_id: "inv-#{order.id}",
279
+ order_description: 'Pro Plan - Monthly',
280
+ success_url: 'https://example.com/thank-you',
281
+ cancel_url: 'https://example.com/checkout',
282
+ ipn_callback_url: 'https://example.com/webhooks/nowpayments'
283
+ )
284
+ # => {
285
+ # "id" => "5824448584",
286
+ # "order_id" => "inv-123",
287
+ # "order_description" => "Pro Plan - Monthly",
288
+ # "price_amount" => 99.0,
289
+ # "price_currency" => "usd",
290
+ # "invoice_url" => "https://nowpayments.io/payment/?iid=5824448584",
291
+ # "created_at" => "2025-11-01T12:00:00.000Z"
292
+ # }
293
+
294
+ # Redirect customer to invoice_url
295
+ redirect_to invoice['invoice_url']
296
+ ```
297
+
298
+ ### `invoice(invoice_id)`
299
+
300
+ Get invoice details and payment status.
301
+
302
+ ```ruby
303
+ invoice = client.invoice('5824448584')
304
+ # => {
305
+ # "id" => "5824448584",
306
+ # "payment_status" => "finished",
307
+ # "pay_currency" => "btc",
308
+ # "pay_amount" => 0.00234567,
309
+ # ...
310
+ # }
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Estimates API
316
+
317
+ ### `estimate(amount:, currency_from:, currency_to:)`
318
+
319
+ Estimate exchange amount between currencies.
320
+
321
+ ```ruby
322
+ estimate = client.estimate(
323
+ amount: 100,
324
+ currency_from: 'usd',
325
+ currency_to: 'btc'
326
+ )
327
+ # => {
328
+ # "currency_from" => "usd",
329
+ # "amount_from" => 100,
330
+ # "currency_to" => "btc",
331
+ # "estimated_amount" => 0.00234567
332
+ # }
333
+ ```
334
+
335
+ ---
336
+
337
+ ## Recurring Payments API
338
+
339
+ Create and manage subscription billing.
340
+
341
+ ### `create_recurring_payment(price_amount:, price_currency:, pay_currency:, **params)`
342
+
343
+ Create a recurring payment plan.
344
+
345
+ ```ruby
346
+ subscription = client.create_recurring_payment(
347
+ price_amount: 29.99,
348
+ price_currency: 'usd',
349
+ pay_currency: 'btc',
350
+ order_id: "sub-#{subscription.id}",
351
+ order_description: 'Monthly Subscription',
352
+ period: 'month', # 'day', 'week', 'month', 'year'
353
+ ipn_callback_url: 'https://example.com/webhooks/nowpayments'
354
+ )
355
+ # => {
356
+ # "id" => "recurring_123",
357
+ # "order_id" => "sub-456",
358
+ # "price_amount" => 29.99,
359
+ # "price_currency" => "usd",
360
+ # "pay_currency" => "btc",
361
+ # "period" => "month",
362
+ # "status" => "active"
363
+ # }
364
+ ```
365
+
366
+ ### `recurring_payment(recurring_payment_id)`
367
+
368
+ Get recurring payment details.
369
+
370
+ ```ruby
371
+ subscription = client.recurring_payment('recurring_123')
372
+ # => {
373
+ # "id" => "recurring_123",
374
+ # "status" => "active",
375
+ # "next_payment_date" => "2025-12-01T00:00:00.000Z",
376
+ # ...
377
+ # }
378
+ ```
379
+
380
+ ### `delete_recurring_payment(recurring_payment_id)`
381
+
382
+ **Requires JWT authentication.**
383
+
384
+ Cancel a recurring payment.
385
+
386
+ ```ruby
387
+ # Authenticate first
388
+ client.authenticate(email: 'your@email.com', password: 'password')
389
+
390
+ # Delete recurring payment
391
+ result = client.delete_recurring_payment('recurring_123')
392
+ # => {"result" => true}
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Mass Payouts API
398
+
399
+ **All payout endpoints require JWT authentication.**
400
+
401
+ ### `create_payout(withdrawals:, **params)`
402
+
403
+ **Requires JWT authentication.**
404
+
405
+ Create a batch payout to multiple addresses.
406
+
407
+ ```ruby
408
+ # Authenticate first
409
+ client.authenticate(email: 'your@email.com', password: 'password')
410
+
411
+ # Create payout
412
+ payout = client.create_payout(
413
+ withdrawals: [
414
+ {
415
+ address: 'TEmGwPeRTPiLFLVfBxXkSP91yc5GMNQhfS',
416
+ currency: 'trx',
417
+ amount: 10,
418
+ extra_id: nil # Required for some currencies (XRP, XLM, etc.)
419
+ },
420
+ {
421
+ address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb',
422
+ currency: 'eth',
423
+ amount: 0.1
424
+ }
425
+ ],
426
+ payout_description: 'Weekly affiliate payouts'
427
+ )
428
+ # => {
429
+ # "id" => "batch_123",
430
+ # "withdrawals" => [
431
+ # {
432
+ # "id" => "withdrawal_456",
433
+ # "address" => "TEmGw...",
434
+ # "currency" => "trx",
435
+ # "amount" => 10,
436
+ # "status" => "pending"
437
+ # },
438
+ # ...
439
+ # ]
440
+ # }
441
+ ```
442
+
443
+ ### `verify_payout(batch_withdrawal_id:, verification_code:)`
444
+
445
+ **Requires JWT authentication.**
446
+
447
+ Verify payout with 2FA code (from Google Authenticator or email).
448
+
449
+ ```ruby
450
+ # Authenticate first
451
+ client.authenticate(email: 'your@email.com', password: 'password')
452
+
453
+ # Verify with 2FA code
454
+ result = client.verify_payout(
455
+ batch_withdrawal_id: 'batch_123',
456
+ verification_code: '123456' # From Google Authenticator
457
+ )
458
+ # => {"result" => true}
459
+ ```
460
+
461
+ ### `list_payouts(limit:, offset:)`
462
+
463
+ **Requires JWT authentication.**
464
+
465
+ List all payouts.
466
+
467
+ ```ruby
468
+ # Authenticate first
469
+ client.authenticate(email: 'your@email.com', password: 'password')
470
+
471
+ # List payouts
472
+ payouts = client.list_payouts(limit: 10, offset: 0)
473
+ # => {
474
+ # "count" => 5,
475
+ # "data" => [
476
+ # {
477
+ # "id" => "batch_123",
478
+ # "status" => "verified",
479
+ # "created_at" => "2025-11-01T12:00:00.000Z",
480
+ # ...
481
+ # },
482
+ # ...
483
+ # ]
484
+ # }
485
+ ```
486
+
487
+ ---
488
+
489
+ ## Conversions API
490
+
491
+ **All conversion endpoints require JWT authentication.**
492
+
493
+ Convert between cryptocurrencies at market rates.
494
+
495
+ ### `create_conversion(from_currency:, to_currency:, amount:)`
496
+
497
+ **Requires JWT authentication.**
498
+
499
+ Create a currency conversion.
500
+
501
+ ```ruby
502
+ # Authenticate first
503
+ client.authenticate(email: 'your@email.com', password: 'password')
504
+
505
+ # Create conversion
506
+ conversion = client.create_conversion(
507
+ from_currency: 'btc',
508
+ to_currency: 'eth',
509
+ amount: 0.1
510
+ )
511
+ # => {
512
+ # "conversion_id" => "conversion_123",
513
+ # "from_currency" => "btc",
514
+ # "to_currency" => "eth",
515
+ # "from_amount" => 0.1,
516
+ # "to_amount" => 2.5,
517
+ # "status" => "processing"
518
+ # }
519
+ ```
520
+
521
+ ### `conversion_status(conversion_id)`
522
+
523
+ **Requires JWT authentication.**
524
+
525
+ Check conversion status.
526
+
527
+ ```ruby
528
+ # Authenticate first
529
+ client.authenticate(email: 'your@email.com', password: 'password')
530
+
531
+ # Check status
532
+ status = client.conversion_status('conversion_123')
533
+ # => {
534
+ # "conversion_id" => "conversion_123",
535
+ # "status" => "completed",
536
+ # "from_currency" => "btc",
537
+ # "to_currency" => "eth",
538
+ # "from_amount" => 0.1,
539
+ # "to_amount" => 2.5
540
+ # }
541
+ ```
542
+
543
+ ### `list_conversions(limit:, offset:)`
544
+
545
+ **Requires JWT authentication.**
546
+
547
+ List all conversions.
548
+
549
+ ```ruby
550
+ # Authenticate first
551
+ client.authenticate(email: 'your@email.com', password: 'password')
552
+
553
+ # List conversions
554
+ conversions = client.list_conversions(limit: 10, offset: 0)
555
+ # => {
556
+ # "count" => 3,
557
+ # "data" => [
558
+ # {
559
+ # "conversion_id" => "conversion_123",
560
+ # "status" => "completed",
561
+ # "created_at" => "2025-11-01T12:00:00.000Z",
562
+ # ...
563
+ # },
564
+ # ...
565
+ # ]
566
+ # }
567
+ ```
568
+
569
+ ---
570
+
571
+ ## Custody API
572
+
573
+ Manage sub-accounts for users (marketplaces, casinos, exchanges).
574
+
575
+ ### `create_sub_account(user_id:)`
576
+
577
+ Create a sub-account for a user.
578
+
579
+ ```ruby
580
+ sub_account = client.create_sub_account(user_id: 'user_123')
581
+ # => {
582
+ # "result" => {
583
+ # "id" => 123456,
584
+ # "user_id" => "user_123",
585
+ # "created_at" => "2025-11-01T12:00:00.000Z"
586
+ # }
587
+ # }
588
+ ```
589
+
590
+ ### `create_sub_account_deposit(user_id:, currency:)`
591
+
592
+ Generate deposit address for user's wallet.
593
+
594
+ ```ruby
595
+ deposit = client.create_sub_account_deposit(
596
+ user_id: 'user_123',
597
+ currency: 'btc'
598
+ )
599
+ # => {
600
+ # "result" => {
601
+ # "user_id" => "user_123",
602
+ # "currency" => "btc",
603
+ # "address" => "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
604
+ # }
605
+ # }
606
+ ```
607
+
608
+ ### `sub_account_balances(user_id:)`
609
+
610
+ Get user's sub-account balance.
611
+
612
+ ```ruby
613
+ balances = client.sub_account_balances(user_id: 'user_123')
614
+ # => {
615
+ # "result" => {
616
+ # "balances" => {
617
+ # "btc" => 0.5,
618
+ # "eth" => 10.0,
619
+ # "usdt" => 1000.0
620
+ # }
621
+ # }
622
+ # }
623
+ ```
624
+
625
+ ### `transfer_to_sub_account(user_id:, currency:, amount:)`
626
+
627
+ Transfer funds from main account to sub-account.
628
+
629
+ ```ruby
630
+ transfer = client.transfer_to_sub_account(
631
+ user_id: 'user_123',
632
+ currency: 'btc',
633
+ amount: 0.1
634
+ )
635
+ # => {
636
+ # "result" => {
637
+ # "id" => "transfer_456",
638
+ # "user_id" => "user_123",
639
+ # "currency" => "btc",
640
+ # "amount" => 0.1,
641
+ # "status" => "completed"
642
+ # }
643
+ # }
644
+ ```
645
+
646
+ ### `transfer_between_sub_accounts(currency:, amount:, from_id:, to_id:)`
647
+
648
+ **Requires JWT authentication.**
649
+
650
+ Transfer between two sub-accounts.
651
+
652
+ ```ruby
653
+ # Authenticate first
654
+ client.authenticate(email: 'your@email.com', password: 'password')
655
+
656
+ # Transfer between users
657
+ transfer = client.transfer_between_sub_accounts(
658
+ currency: 'btc',
659
+ amount: 0.05,
660
+ from_id: 'user_123',
661
+ to_id: 'user_456'
662
+ )
663
+ # => {
664
+ # "result" => {
665
+ # "id" => "transfer_789",
666
+ # "from_id" => "user_123",
667
+ # "to_id" => "user_456",
668
+ # "currency" => "btc",
669
+ # "amount" => 0.05,
670
+ # "status" => "completed"
671
+ # }
672
+ # }
673
+ ```
674
+
675
+ ### `withdraw_from_sub_account(user_id:, currency:, amount:, address:)`
676
+
677
+ Withdraw funds from sub-account to external address.
678
+
679
+ ```ruby
680
+ withdrawal = client.withdraw_from_sub_account(
681
+ user_id: 'user_123',
682
+ currency: 'btc',
683
+ amount: 0.05,
684
+ address: 'bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh'
685
+ )
686
+ # => {
687
+ # "result" => {
688
+ # "id" => "withdrawal_789",
689
+ # "user_id" => "user_123",
690
+ # "currency" => "btc",
691
+ # "amount" => 0.05,
692
+ # "address" => "bc1q...",
693
+ # "status" => "processing"
694
+ # }
695
+ # }
696
+ ```
697
+
698
+ ### `write_off_sub_account_balance(user_id:, currency:, amount:, external_id:)`
699
+
700
+ **Requires JWT authentication.**
701
+
702
+ Write off (deduct) balance from sub-account.
703
+
704
+ ```ruby
705
+ # Authenticate first
706
+ client.authenticate(email: 'your@email.com', password: 'password')
707
+
708
+ # Write off balance
709
+ result = client.write_off_sub_account_balance(
710
+ user_id: 'user_123',
711
+ currency: 'btc',
712
+ amount: 0.01,
713
+ external_id: 'fee_charge_789'
714
+ )
715
+ # => {
716
+ # "result" => {
717
+ # "user_id" => "user_123",
718
+ # "currency" => "btc",
719
+ # "amount" => 0.01,
720
+ # "external_id" => "fee_charge_789",
721
+ # "status" => "completed"
722
+ # }
723
+ # }
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Error Handling
729
+
730
+ The SDK raises specific exceptions for different error types:
731
+
732
+ ```ruby
733
+ begin
734
+ payment = client.create_payment(...)
735
+
736
+ rescue NOWPayments::AuthenticationError => e
737
+ # 401 - Invalid API key
738
+ puts "Authentication failed: #{e.message}"
739
+
740
+ rescue NOWPayments::BadRequestError => e
741
+ # 400 - Invalid parameters
742
+ puts "Bad request: #{e.message}"
743
+ puts "Details: #{e.body}"
744
+
745
+ rescue NOWPayments::NotFoundError => e
746
+ # 404 - Resource not found
747
+ puts "Not found: #{e.message}"
748
+
749
+ rescue NOWPayments::UnprocessableEntityError => e
750
+ # 422 - Validation errors
751
+ puts "Validation failed: #{e.message}"
752
+
753
+ rescue NOWPayments::RateLimitError => e
754
+ # 429 - Too many requests
755
+ retry_after = e.headers['Retry-After']
756
+ puts "Rate limited. Retry after #{retry_after} seconds"
757
+
758
+ rescue NOWPayments::ServerError => e
759
+ # 500 - NOWPayments server error
760
+ puts "Server error: #{e.message}"
761
+
762
+ rescue NOWPayments::SecurityError => e
763
+ # Webhook signature verification failed
764
+ puts "Security error: #{e.message}"
765
+
766
+ rescue NOWPayments::ConnectionError => e
767
+ # Network/connection error
768
+ puts "Connection error: #{e.message}"
769
+
770
+ rescue NOWPayments::Error => e
771
+ # Generic API error
772
+ puts "API error: #{e.message}"
773
+ end
774
+ ```
775
+
776
+ **Exception hierarchy:**
777
+
778
+ ```
779
+ NOWPayments::Error
780
+ ├── NOWPayments::AuthenticationError (401)
781
+ ├── NOWPayments::BadRequestError (400)
782
+ ├── NOWPayments::NotFoundError (404)
783
+ ├── NOWPayments::UnprocessableEntityError (422)
784
+ ├── NOWPayments::RateLimitError (429)
785
+ ├── NOWPayments::ServerError (5xx)
786
+ ├── NOWPayments::SecurityError (webhook verification)
787
+ └── NOWPayments::ConnectionError (network)
788
+ ```
789
+
790
+ ---
791
+
792
+ ## Webhook Verification
793
+
794
+ **Critical:** Always verify webhook signatures to prevent fraud.
795
+
796
+ ```ruby
797
+ # Rails controller
798
+ class WebhooksController < ApplicationController
799
+ skip_before_action :verify_authenticity_token
800
+
801
+ def nowpayments
802
+ # Verify signature - raises SecurityError if invalid
803
+ payload = NOWPayments::Rack.verify_webhook(
804
+ request,
805
+ ENV['NOWPAYMENTS_IPN_SECRET']
806
+ )
807
+
808
+ # Process payment
809
+ order = Order.find_by(id: payload['order_id'])
810
+
811
+ case payload['payment_status']
812
+ when 'finished'
813
+ order.mark_paid!
814
+ when 'failed', 'expired'
815
+ order.cancel!
816
+ end
817
+
818
+ head :ok
819
+
820
+ rescue NOWPayments::SecurityError => e
821
+ logger.error "Invalid webhook: #{e.message}"
822
+ head :forbidden
823
+ end
824
+ end
825
+ ```
826
+
827
+ **Signature verification (low-level):**
828
+
829
+ ```ruby
830
+ require 'openssl'
831
+
832
+ def verify_signature(payload, signature, secret)
833
+ hmac = OpenSSL::HMAC.hexdigest('SHA512', secret, payload)
834
+
835
+ # Use constant-time comparison to prevent timing attacks
836
+ secure_compare(hmac, signature)
837
+ end
838
+
839
+ def secure_compare(a, b)
840
+ return false unless a.bytesize == b.bytesize
841
+
842
+ l = a.unpack("C*")
843
+ r = 0
844
+ i = -1
845
+
846
+ b.each_byte { |byte| r |= byte ^ l[i += 1] }
847
+ r == 0
848
+ end
849
+ ```
850
+
851
+ ---
852
+
853
+ ## Rate Limits
854
+
855
+ **Standard limits:**
856
+ - 60 requests per minute (standard endpoints)
857
+ - 10 requests per minute (payment creation)
858
+
859
+ **Best practices:**
860
+ - Implement exponential backoff on rate limit errors
861
+ - Cache currency lists and static data
862
+ - Use batch operations (payouts, invoices) when possible
863
+ - Monitor `Retry-After` header in rate limit responses
864
+
865
+ ```ruby
866
+ def with_retry(max_retries: 3)
867
+ retries = 0
868
+
869
+ begin
870
+ yield
871
+ rescue NOWPayments::RateLimitError => e
872
+ retries += 1
873
+ if retries <= max_retries
874
+ retry_after = e.headers['Retry-After'].to_i
875
+ sleep retry_after
876
+ retry
877
+ else
878
+ raise
879
+ end
880
+ end
881
+ end
882
+
883
+ # Usage
884
+ with_retry do
885
+ client.create_payment(...)
886
+ end
887
+ ```
888
+
889
+ ---
890
+
891
+ ## Testing
892
+
893
+ **Use sandbox for development:**
894
+
895
+ ```ruby
896
+ client = NOWPayments::Client.new(
897
+ api_key: ENV['NOWPAYMENTS_SANDBOX_API_KEY'],
898
+ sandbox: true
899
+ )
900
+ ```
901
+
902
+ **Get sandbox credentials:**
903
+ 1. Create account at https://account-sandbox.nowpayments.io/
904
+ 2. Generate API key from dashboard
905
+ 3. Generate IPN secret for webhooks
906
+ 4. Test with sandbox cryptocurrencies
907
+
908
+ **Sandbox test currencies:**
909
+ - All major cryptocurrencies available
910
+ - Instant confirmations (no waiting)
911
+ - Test payouts without real funds
912
+ - Test webhooks with ngrok
913
+
914
+ ---
915
+
916
+ ## Support
917
+
918
+ - [GitHub Issues](https://github.com/Sentia/nowpayments/issues)
919
+ - [NOWPayments Support](https://nowpayments.io/help)
920
+ - [API Documentation](https://documenter.getpostman.com/view/7907941/2s93JusNJt)
921
+ - [Sandbox Dashboard](https://account-sandbox.nowpayments.io/)
922
+ - [Production Dashboard](https://nowpayments.io/)