paystack_sdk 0.1.0 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +624 -86
- data/lib/paystack_sdk/client.rb +72 -14
- data/lib/paystack_sdk/middleware/transport_errors.rb +23 -0
- data/lib/paystack_sdk/request_helpers.rb +117 -0
- data/lib/paystack_sdk/resources/banks.rb +163 -13
- data/lib/paystack_sdk/resources/base.rb +4 -2
- data/lib/paystack_sdk/resources/charges.rb +219 -0
- data/lib/paystack_sdk/resources/customers.rb +262 -145
- data/lib/paystack_sdk/resources/extensions/charges.rb +65 -0
- data/lib/paystack_sdk/resources/miscellaneous.rb +58 -0
- data/lib/paystack_sdk/resources/refunds.rb +116 -0
- data/lib/paystack_sdk/resources/transactions.rb +365 -238
- data/lib/paystack_sdk/resources/transfer_recipients.rb +135 -27
- data/lib/paystack_sdk/resources/transfers.rb +260 -25
- data/lib/paystack_sdk/response.rb +95 -18
- data/lib/paystack_sdk/utils/connection_utils.rb +100 -5
- data/lib/paystack_sdk/validations.rb +24 -20
- data/lib/paystack_sdk/version.rb +1 -1
- data/lib/paystack_sdk/webhook.rb +152 -0
- data/lib/paystack_sdk.rb +27 -2
- metadata +70 -16
- data/lib/paystack_sdk/resources/verification.rb +0 -36
data/README.md
CHANGED
|
@@ -8,12 +8,21 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
|
|
|
8
8
|
- [Quick Start](#quick-start)
|
|
9
9
|
- [Usage](#usage)
|
|
10
10
|
- [Client Initialization](#client-initialization)
|
|
11
|
+
- [Test and Live Keys](#test-and-live-keys)
|
|
11
12
|
- [Transactions](#transactions)
|
|
12
13
|
- [Initialize a Transaction](#initialize-a-transaction)
|
|
13
14
|
- [Verify a Transaction](#verify-a-transaction)
|
|
14
15
|
- [List Transactions](#list-transactions)
|
|
15
16
|
- [Fetch a Transaction](#fetch-a-transaction)
|
|
16
17
|
- [Get Transaction Totals](#get-transaction-totals)
|
|
18
|
+
- [Checking a Payment](#checking-a-payment)
|
|
19
|
+
- [Charging a Saved Card](#charging-a-saved-card)
|
|
20
|
+
- [Charges](#charges)
|
|
21
|
+
- [Mobile Money in Ghana: Which Networks](#mobile-money-in-ghana-which-networks)
|
|
22
|
+
- [Create a Mobile Money Charge](#create-a-mobile-money-charge)
|
|
23
|
+
- [Create a Charge on Another Channel](#create-a-charge-on-another-channel)
|
|
24
|
+
- [Complete a Charge](#complete-a-charge)
|
|
25
|
+
- [Check a Pending Charge](#check-a-pending-charge)
|
|
17
26
|
- [Customers](#customers)
|
|
18
27
|
- [Create a Customer](#create-a-customer)
|
|
19
28
|
- [List Customers](#list-customers)
|
|
@@ -22,14 +31,27 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
|
|
|
22
31
|
- [Validate a Customer](#validate-a-customer)
|
|
23
32
|
- [Set Risk Action](#set-risk-action)
|
|
24
33
|
- [Deactivate Authorization](#deactivate-authorization)
|
|
34
|
+
- [Banks](#banks)
|
|
35
|
+
- [Miscellaneous](#miscellaneous)
|
|
36
|
+
- [Transfer Recipients](#transfer-recipients)
|
|
37
|
+
- [Transfers](#transfers)
|
|
38
|
+
- [Initiate a Transfer](#initiate-a-transfer)
|
|
39
|
+
- [Verify, Fetch and List Transfers](#verify-fetch-and-list-transfers)
|
|
40
|
+
- [Bulk Transfers and the OTP Requirement](#bulk-transfers-and-the-otp-requirement)
|
|
41
|
+
- [Refunds](#refunds)
|
|
42
|
+
- [Create a Refund](#create-a-refund)
|
|
43
|
+
- [Fetch, List and Retry Refunds](#fetch-list-and-retry-refunds)
|
|
25
44
|
- [Response Handling](#response-handling)
|
|
26
45
|
- [Working with Response Objects](#working-with-response-objects)
|
|
27
46
|
- [Accessing the Original Response](#accessing-the-original-response)
|
|
28
47
|
- [Error Handling](#error-handling)
|
|
29
48
|
- [Advanced Usage](#advanced-usage)
|
|
49
|
+
- [Timeouts and Retries](#timeouts-and-retries)
|
|
50
|
+
- [Webhooks](#webhooks)
|
|
30
51
|
- [Environment Variables](#environment-variables)
|
|
31
52
|
- [Direct Resource Instantiation](#direct-resource-instantiation)
|
|
32
53
|
- [Development](#development)
|
|
54
|
+
- [Style and Linting](#style-and-linting)
|
|
33
55
|
- [Contributing](#contributing)
|
|
34
56
|
- [License](#license)
|
|
35
57
|
- [Code of Conduct](#code-of-conduct)
|
|
@@ -63,14 +85,12 @@ require 'paystack_sdk'
|
|
|
63
85
|
paystack = PaystackSdk::Client.new(secret_key: "sk_test_xxx")
|
|
64
86
|
|
|
65
87
|
# Initialize a transaction
|
|
66
|
-
params = {
|
|
67
|
-
email: "customer@email.com",
|
|
68
|
-
amount: 2300, # Amount in the smallest currency unit (kobo for NGN)
|
|
69
|
-
currency: "NGN"
|
|
70
|
-
}
|
|
71
|
-
|
|
72
88
|
begin
|
|
73
|
-
response = paystack.transactions.initiate(
|
|
89
|
+
response = paystack.transactions.initiate(
|
|
90
|
+
email: "customer@email.com",
|
|
91
|
+
amount: 2300, # Amount in the smallest currency unit (kobo for NGN)
|
|
92
|
+
currency: "NGN"
|
|
93
|
+
)
|
|
74
94
|
|
|
75
95
|
if response.success?
|
|
76
96
|
puts "Visit this URL to complete payment: #{response.authorization_url}"
|
|
@@ -78,21 +98,19 @@ begin
|
|
|
78
98
|
else
|
|
79
99
|
puts "Error: #{response.error_message}"
|
|
80
100
|
end
|
|
81
|
-
rescue
|
|
101
|
+
rescue ArgumentError => e
|
|
82
102
|
puts "Missing required data: #{e.message}"
|
|
83
103
|
rescue PaystackSdk::InvalidFormatError => e
|
|
84
104
|
puts "Invalid data format: #{e.message}"
|
|
85
105
|
end
|
|
86
106
|
|
|
87
107
|
# Create a customer
|
|
88
|
-
customer_params = {
|
|
89
|
-
email: "customer@email.com",
|
|
90
|
-
first_name: "John",
|
|
91
|
-
last_name: "Doe"
|
|
92
|
-
}
|
|
93
|
-
|
|
94
108
|
begin
|
|
95
|
-
customer_response = paystack.customers.create(
|
|
109
|
+
customer_response = paystack.customers.create(
|
|
110
|
+
email: "customer@email.com",
|
|
111
|
+
first_name: "John",
|
|
112
|
+
last_name: "Doe"
|
|
113
|
+
)
|
|
96
114
|
|
|
97
115
|
if customer_response.success?
|
|
98
116
|
puts "Customer created: #{customer_response.data.customer_code}"
|
|
@@ -121,10 +139,10 @@ The SDK validates your parameters **before** making API calls and throws excepti
|
|
|
121
139
|
|
|
122
140
|
```ruby
|
|
123
141
|
begin
|
|
124
|
-
#
|
|
125
|
-
response = paystack.transactions.initiate(
|
|
126
|
-
rescue
|
|
127
|
-
puts "Fix your data: #{e.message}"
|
|
142
|
+
# Ruby raises ArgumentError for a missing required keyword, before any API call
|
|
143
|
+
response = paystack.transactions.initiate(amount: 1000) # Missing required email
|
|
144
|
+
rescue ArgumentError => e
|
|
145
|
+
puts "Fix your data: #{e.message}" # => "missing keyword: :email"
|
|
128
146
|
end
|
|
129
147
|
```
|
|
130
148
|
|
|
@@ -133,7 +151,7 @@ end
|
|
|
133
151
|
All successful API calls return a `Response` object that you can check for success:
|
|
134
152
|
|
|
135
153
|
```ruby
|
|
136
|
-
response = paystack.transactions.initiate(valid_params)
|
|
154
|
+
response = paystack.transactions.initiate(**valid_params)
|
|
137
155
|
|
|
138
156
|
if response.success?
|
|
139
157
|
puts "Transaction created: #{response.authorization_url}"
|
|
@@ -151,9 +169,9 @@ end
|
|
|
151
169
|
|
|
152
170
|
- **Validation errors** - when required parameters are missing or have invalid formats
|
|
153
171
|
- **Authentication errors** (401) - usually configuration issues
|
|
154
|
-
- **Rate limiting** (429) -
|
|
172
|
+
- **Rate limiting** (429) - raised after automatic retries are exhausted, or immediately if Paystack asks for a long wait (see [Timeouts and Retries](#timeouts-and-retries))
|
|
155
173
|
- **Server errors** (5xx) - Paystack infrastructure issues
|
|
156
|
-
- **Network errors** - connection failures
|
|
174
|
+
- **Network errors** - timeouts and connection failures, raised as `PaystackSdk::TimeoutError` / `PaystackSdk::ConnectionError`
|
|
157
175
|
|
|
158
176
|
All other API errors (resource not found, business logic errors, etc.) are returned as unsuccessful Response objects.
|
|
159
177
|
|
|
@@ -172,6 +190,19 @@ paystack = PaystackSdk::Client.new # => This will dynamically fetch the secret k
|
|
|
172
190
|
connection = paystack.connection
|
|
173
191
|
```
|
|
174
192
|
|
|
193
|
+
### Test and Live Keys
|
|
194
|
+
|
|
195
|
+
A live key (`sk_live_...`) moves real money. `client.live?` tells you which kind a client holds, and `sandbox_only: true` makes the client refuse anything but a test key (`sk_test_...`) at construction, before any request is sent. Set it in staging and CI:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
client = PaystackSdk::Client.new(secret_key: ENV["PAYSTACK_SECRET_KEY"], sandbox_only: true)
|
|
199
|
+
# => ArgumentError if the key is a live key, or anything not recognisable as a test key
|
|
200
|
+
|
|
201
|
+
client.live? # => false
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The error never includes the key. A pre-built Faraday connection is checked too (the key is read from its `Authorization` header).
|
|
205
|
+
|
|
175
206
|
### Transactions
|
|
176
207
|
|
|
177
208
|
The SDK provides comprehensive support for Paystack's Transaction API.
|
|
@@ -179,16 +210,13 @@ The SDK provides comprehensive support for Paystack's Transaction API.
|
|
|
179
210
|
#### Initialize a Transaction
|
|
180
211
|
|
|
181
212
|
```ruby
|
|
182
|
-
#
|
|
183
|
-
|
|
213
|
+
# Amount is in the smallest currency unit (e.g., kobo, pesewas, cents)
|
|
214
|
+
response = paystack.transactions.initiate(
|
|
184
215
|
email: "customer@example.com",
|
|
185
|
-
amount: 10000,
|
|
216
|
+
amount: 10000,
|
|
186
217
|
currency: "GHS",
|
|
187
218
|
callback_url: "https://example.com/callback"
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
# Initialize the transaction
|
|
191
|
-
response = paystack.transactions.initiate(params)
|
|
219
|
+
)
|
|
192
220
|
|
|
193
221
|
if response.success?
|
|
194
222
|
puts "Transaction initialized successfully!"
|
|
@@ -200,6 +228,113 @@ else
|
|
|
200
228
|
end
|
|
201
229
|
```
|
|
202
230
|
|
|
231
|
+
### Charges
|
|
232
|
+
|
|
233
|
+
The Charge API lets you pick the payment channel yourself instead of sending the customer to Checkout: a saved card authorization, a bank account, USSD, mobile money, QR, EFT, Pay with Transfer or Capitec Pay. Many charges need one more step from the customer (a PIN, OTP, phone number, birthday or address) before they complete. See the Paystack docs: [Charge API](https://paystack.com/docs/api/charge/) and [Payment Channels](https://paystack.com/docs/payments/payment-channels/).
|
|
234
|
+
|
|
235
|
+
#### Mobile Money in Ghana: Which Networks
|
|
236
|
+
|
|
237
|
+
Paystack lists three Ghana mobile-money providers (`banks.list(country: "ghana", type: "mobile_money")`): `MTN`, `VOD` (shown as Vodafone, now Telecel) and `ATL` (AirtelTigo). The bank list uses uppercase codes; `charges.mobile_money` takes the provider in any case and sends it in lowercase (`mtn`, `vod`, `atl`).
|
|
238
|
+
|
|
239
|
+
What was checked, against Paystack's test API only: a merchant-started charge of 100 pesewas (GHS) for each of the three providers on Paystack's test number was accepted, came back with `status: "success"` and `gateway_response: "Approved"` immediately, and `check_pending(reference:)` returned the same. **Test mode does not model the payer approving the prompt on their phone, so this does not show which live networks accept a charge you start yourself.** Treat each network as unverified in live mode: start the charge, read `response.status` (`status?(:pay_offline)`, `status?(:send_otp)`, `status?(:pending)` and so on, as Paystack returns them), show `response.display_text` to the payer when there is one, and poll `charges.check_pending(reference:)` or wait for the `charge.success` webhook. Confirm with `transactions.verify(reference:)` before giving value. If a network refuses a merchant-started prompt, fall back to a payment link for that network.
|
|
240
|
+
|
|
241
|
+
#### Create a Mobile Money Charge
|
|
242
|
+
|
|
243
|
+
Mobile money is available to businesses in Ghana, Kenya and Côte d'Ivoire. `mobile_money` checks the `mobile_money` object (phone or till account, and a known provider) and sends it with `create`.
|
|
244
|
+
|
|
245
|
+
Supported providers (case-insensitive): `mtn`, `atl` (ATMoney/Airtel Money), `vod` (Telecel, formerly Vodafone), `mpesa`, `mpesa_offline`, `mptill` (M-PESA Till: send `account:`, the till number, instead of `phone:`), `orange`, `wave`.
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
paystack = PaystackSdk::Client.new(secret_key: "sk_test_xxx")
|
|
249
|
+
|
|
250
|
+
response = paystack.charges.mobile_money(
|
|
251
|
+
email: "customer@email.com",
|
|
252
|
+
amount: 100, # smallest unit (pesewas/cent)
|
|
253
|
+
currency: "GHS", # optional; Paystack uses your integration's currency without it
|
|
254
|
+
mobile_money: {
|
|
255
|
+
phone: "0551234987",
|
|
256
|
+
provider: "mtn" # mtn | atl | vod | mpesa | mpesa_offline | mptill | orange | wave
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if response.success?
|
|
261
|
+
case response.status
|
|
262
|
+
when "pay_offline"
|
|
263
|
+
# Show instruction text and wait for webhook or verify later
|
|
264
|
+
puts response.display_text
|
|
265
|
+
when "send_otp"
|
|
266
|
+
# For Vodafone, collect voucher/OTP and submit below
|
|
267
|
+
puts response.display_text
|
|
268
|
+
when "success"
|
|
269
|
+
puts "Charge completed: #{response.reference}"
|
|
270
|
+
else
|
|
271
|
+
puts "Status: #{response.status}"
|
|
272
|
+
end
|
|
273
|
+
else
|
|
274
|
+
puts "Charge failed: #{response.error_message}"
|
|
275
|
+
end
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
#### Create a Charge on Another Channel
|
|
279
|
+
|
|
280
|
+
`create` takes one channel object (or an `authorization_code`) as a keyword hash and sends it as Paystack documents it.
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
# A returning customer's saved card
|
|
284
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, authorization_code: "AUTH_xxxx")
|
|
285
|
+
|
|
286
|
+
# A bank account (Paystack may then ask for the customer's birthday or an OTP)
|
|
287
|
+
paystack.charges.create(
|
|
288
|
+
email: "customer@email.com",
|
|
289
|
+
amount: 10000,
|
|
290
|
+
bank: {code: "057", account_number: "0000000000"},
|
|
291
|
+
birthday: Date.new(1995, 12, 23) # or "1995-12-23"
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
# USSD (Nigeria), Pay with Transfer, and QR or EFT (South Africa)
|
|
295
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, ussd: {type: "737"})
|
|
296
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, bank_transfer: {account_expires_at: "2026-10-10T12:00:00Z"})
|
|
297
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, currency: "ZAR", qr: {provider: "scan-to-pay"})
|
|
298
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, currency: "ZAR", eft: {provider: "ozow"})
|
|
299
|
+
|
|
300
|
+
# Send the payment through a split or to a subaccount
|
|
301
|
+
paystack.charges.create(email: "customer@email.com", amount: 10000, authorization_code: "AUTH_xxxx", split_code: "SPL_xxxx")
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
#### Complete a Charge
|
|
305
|
+
|
|
306
|
+
When the response asks for more from the customer (read `response.status` and `response.display_text`), send it with the charge's `reference`:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
paystack.charges.submit_pin(pin: "1234", reference: "5bwib5v6anhe9xa")
|
|
310
|
+
paystack.charges.submit_otp(otp: "123456", reference: "5bwib5v6anhe9xa") # e.g. a Vodafone voucher
|
|
311
|
+
paystack.charges.submit_phone(phone: "08012345678", reference: "5bwib5v6anhe9xa")
|
|
312
|
+
paystack.charges.submit_birthday(birthday: "1961-09-21", reference: "5bwib5v6anhe9xa")
|
|
313
|
+
paystack.charges.submit_address(
|
|
314
|
+
address: "140 N 2ND ST",
|
|
315
|
+
city: "Stroudsburg",
|
|
316
|
+
state: "PA",
|
|
317
|
+
zip_code: "18360",
|
|
318
|
+
reference: "7c7rpkqpc0tijs8"
|
|
319
|
+
)
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
#### Check a Pending Charge
|
|
323
|
+
|
|
324
|
+
If a charge comes back `pending`, or a `/charge` call failed with an exception, wait at least 10 seconds, then check it (Paystack warns that checking too early returns more `pending` results):
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
response = paystack.charges.check_pending(reference: "5bwib5v6anhe9xa")
|
|
328
|
+
puts response.status
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
For offline flows such as mobile money, listen for the `charge.success` webhook, and confirm it with `transactions.verify(reference:)` before giving value:
|
|
332
|
+
|
|
333
|
+
```ruby
|
|
334
|
+
verify = paystack.transactions.verify(reference: "r13havfcdt7btcm")
|
|
335
|
+
puts verify.status # "success", "failed", or current state
|
|
336
|
+
```
|
|
337
|
+
|
|
203
338
|
#### Verify a Transaction
|
|
204
339
|
|
|
205
340
|
```ruby
|
|
@@ -231,7 +366,7 @@ end
|
|
|
231
366
|
#### List Transactions
|
|
232
367
|
|
|
233
368
|
```ruby
|
|
234
|
-
# Get all transactions (default pagination: 50 per page)
|
|
369
|
+
# Get all transactions (Paystack's default pagination: 50 per page)
|
|
235
370
|
response = paystack.transactions.list
|
|
236
371
|
|
|
237
372
|
# With custom pagination
|
|
@@ -246,6 +381,9 @@ response = paystack.transactions.list(
|
|
|
246
381
|
status: "success"
|
|
247
382
|
)
|
|
248
383
|
|
|
384
|
+
# Filter by customer (the numeric customer ID, not the CUS_ code)
|
|
385
|
+
response = paystack.transactions.list(customer_id: 12345)
|
|
386
|
+
|
|
249
387
|
if response.success?
|
|
250
388
|
puts "Total transactions: #{response.count}" # response.size is another way
|
|
251
389
|
|
|
@@ -272,8 +410,7 @@ end
|
|
|
272
410
|
|
|
273
411
|
```ruby
|
|
274
412
|
# Fetch a specific transaction by ID
|
|
275
|
-
|
|
276
|
-
response = paystack.transactions.fetch(transaction_id)
|
|
413
|
+
response = paystack.transactions.fetch(id: 12345)
|
|
277
414
|
|
|
278
415
|
if response.success?
|
|
279
416
|
transaction = response.data
|
|
@@ -297,6 +434,9 @@ end
|
|
|
297
434
|
# Get transaction volume and success metrics
|
|
298
435
|
response = paystack.transactions.totals
|
|
299
436
|
|
|
437
|
+
# Within a date range
|
|
438
|
+
response = paystack.transactions.totals(from: "2025-01-01", to: "2025-04-30")
|
|
439
|
+
|
|
300
440
|
if response.success?
|
|
301
441
|
puts "Total Transactions: #{response.data.total_transactions}"
|
|
302
442
|
puts "Total Volume: #{response.data.total_volume}"
|
|
@@ -306,23 +446,79 @@ else
|
|
|
306
446
|
end
|
|
307
447
|
```
|
|
308
448
|
|
|
449
|
+
#### Transaction Timeline, Export and Charging
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
# Timeline of a transaction, by ID or reference
|
|
453
|
+
paystack.transactions.timeline(id: "transaction_reference")
|
|
454
|
+
|
|
455
|
+
# Export transactions (Paystack returns a download link)
|
|
456
|
+
paystack.transactions.export(from: "2025-01-01", to: "2025-04-30", status: "success", settled: true)
|
|
457
|
+
|
|
458
|
+
# Charge a returning customer's saved authorization
|
|
459
|
+
paystack.transactions.charge_authorization(
|
|
460
|
+
email: "customer@example.com",
|
|
461
|
+
amount: 10000,
|
|
462
|
+
authorization_code: "AUTH_xxxx"
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# Debit part of an amount from a saved authorization
|
|
466
|
+
paystack.transactions.partial_debit(
|
|
467
|
+
email: "customer@example.com",
|
|
468
|
+
amount: 5000,
|
|
469
|
+
authorization_code: "AUTH_xxxx",
|
|
470
|
+
currency: "GHS"
|
|
471
|
+
)
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### Checking a Payment
|
|
475
|
+
|
|
476
|
+
Confirm a payment by verifying it, then check the status, amount and currency. `paid?` does all three: it is true only when the call succeeded and the transaction's `status` is `"success"`, and, for the `amount` and `currency` you pass, they match what Paystack reports.
|
|
477
|
+
|
|
478
|
+
```ruby
|
|
479
|
+
response = paystack.transactions.verify(reference: reference)
|
|
480
|
+
|
|
481
|
+
if response.paid?(amount: 5000, currency: "GHS") # amount in the smallest unit, e.g. pesewas
|
|
482
|
+
authorization = response.authorization
|
|
483
|
+
authorization.authorization_code # keep this to charge the card again
|
|
484
|
+
authorization.reusable # true if it can be charged again
|
|
485
|
+
end
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Fields keep Paystack's own names: `response.reference`, `amount`, `currency`, `status`, `paid_at`, `gateway_response`, `fees`, `customer.email`, `customer.customer_code`, and on `authorization`: `authorization_code`, `reusable`, `channel`, `last4`, `card_type`, `signature`, `exp_month`, `exp_year`, `bin`, `bank`, `account_name`, `country_code` and `brand`. `response.status?(:pending)` compares the `status` field with any value, which is how you read a charge that is waiting on the payer (`status?(:send_pin)`, `status?(:pay_offline)`); `response.display_text` is the prompt Paystack wants shown to them.
|
|
489
|
+
|
|
490
|
+
#### Charging a Saved Card
|
|
491
|
+
|
|
492
|
+
After a successful card payment, the `authorization_code` can be charged again with no checkout, for example for a renewal. Only an authorization with `reusable: true` can be charged again.
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
charge = paystack.transactions.charge_authorization(
|
|
496
|
+
email: "customer@example.com", # the customer the authorization belongs to
|
|
497
|
+
amount: 5000,
|
|
498
|
+
currency: "GHS",
|
|
499
|
+
authorization_code: "AUTH_xxxx",
|
|
500
|
+
reference: "renewal-2025-06-ama" # unique per attempt; reuse it if you retry so you cannot charge twice
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
paystack.transactions.verify(reference: "renewal-2025-06-ama").paid?(amount: 5000, currency: "GHS")
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
This was checked against Paystack's test API: a reusable card authorization charged successfully (`status: "success"`, `gateway_response: "Approved"`), and an unknown `authorization_code` came back as an unsuccessful response with `"Authorization code is invalid"`. Writes are never retried after a timeout, so if a `charge_authorization` call times out, verify the `reference` before charging again. Test mode does not prove live behaviour.
|
|
507
|
+
|
|
309
508
|
### Customers
|
|
310
509
|
|
|
311
|
-
The SDK provides comprehensive support for Paystack's Customer API, allowing you to manage customer records and
|
|
510
|
+
The SDK provides comprehensive support for Paystack's Customer API, allowing you to manage customer records, their identity validation, risk actions and authorizations (including Direct Debit mandates).
|
|
312
511
|
|
|
313
512
|
#### Create a Customer
|
|
314
513
|
|
|
315
514
|
```ruby
|
|
316
|
-
|
|
317
|
-
params = {
|
|
515
|
+
response = paystack.customers.create(
|
|
318
516
|
email: "customer@example.com",
|
|
319
517
|
first_name: "John",
|
|
320
518
|
last_name: "Doe",
|
|
321
|
-
phone: "+2348123456789"
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
# Create the customer
|
|
325
|
-
response = paystack.customers.create(params)
|
|
519
|
+
phone: "+2348123456789",
|
|
520
|
+
metadata: {plan: "gold"} # a Hash; Paystack rejects a JSON string here
|
|
521
|
+
)
|
|
326
522
|
|
|
327
523
|
if response.success?
|
|
328
524
|
puts "Customer created successfully!"
|
|
@@ -334,10 +530,12 @@ else
|
|
|
334
530
|
end
|
|
335
531
|
```
|
|
336
532
|
|
|
533
|
+
`first_name`, `last_name` and `phone` are optional, except for customers you will assign a Dedicated Virtual Account to in some business categories (see Paystack's docs).
|
|
534
|
+
|
|
337
535
|
#### List Customers
|
|
338
536
|
|
|
339
537
|
```ruby
|
|
340
|
-
# Get all customers (default pagination: 50 per page)
|
|
538
|
+
# Get all customers (Paystack's default pagination: 50 per page)
|
|
341
539
|
response = paystack.customers.list
|
|
342
540
|
|
|
343
541
|
# With custom pagination
|
|
@@ -351,8 +549,12 @@ response = paystack.customers.list(
|
|
|
351
549
|
to: "2025-06-10"
|
|
352
550
|
)
|
|
353
551
|
|
|
552
|
+
# Cursor pagination: the cursors come back in response.meta
|
|
553
|
+
response = paystack.customers.list(use_cursor: true, per_page: 20)
|
|
554
|
+
response = paystack.customers.list(use_cursor: true, per_page: 20, next_cursor: response.meta.next)
|
|
555
|
+
|
|
354
556
|
if response.success?
|
|
355
|
-
puts "
|
|
557
|
+
puts "Customers on this page: #{response.data.size}"
|
|
356
558
|
|
|
357
559
|
response.data.each do |customer|
|
|
358
560
|
puts "Code: #{customer.customer_code}"
|
|
@@ -369,11 +571,10 @@ end
|
|
|
369
571
|
|
|
370
572
|
```ruby
|
|
371
573
|
# Fetch by customer code
|
|
372
|
-
|
|
373
|
-
response = paystack.customers.fetch(customer_code)
|
|
574
|
+
response = paystack.customers.fetch(email_or_code: "CUS_xr58yrr2ujlft9k")
|
|
374
575
|
|
|
375
|
-
# Or fetch by email
|
|
376
|
-
response = paystack.customers.fetch("customer@example.com")
|
|
576
|
+
# Or fetch by email (Paystack accepts either in the same place)
|
|
577
|
+
response = paystack.customers.fetch(email_or_code: "customer@example.com")
|
|
377
578
|
|
|
378
579
|
if response.success?
|
|
379
580
|
customer = response.data
|
|
@@ -390,14 +591,12 @@ end
|
|
|
390
591
|
#### Update a Customer
|
|
391
592
|
|
|
392
593
|
```ruby
|
|
393
|
-
|
|
394
|
-
|
|
594
|
+
response = paystack.customers.update(
|
|
595
|
+
code: "CUS_xr58yrr2ujlft9k",
|
|
395
596
|
first_name: "Jane",
|
|
396
597
|
last_name: "Smith",
|
|
397
598
|
phone: "+2348987654321"
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
response = paystack.customers.update(customer_code, update_params)
|
|
599
|
+
)
|
|
401
600
|
|
|
402
601
|
if response.success?
|
|
403
602
|
puts "Customer updated successfully!"
|
|
@@ -409,19 +608,19 @@ end
|
|
|
409
608
|
|
|
410
609
|
#### Validate a Customer
|
|
411
610
|
|
|
611
|
+
Paystack only supports `type: "bank_account"` for now, and requires every keyword below; `middle_name` and `value` are optional. Paystack answers `202` and completes the validation asynchronously.
|
|
612
|
+
|
|
412
613
|
```ruby
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
614
|
+
response = paystack.customers.validate(
|
|
615
|
+
code: "CUS_xr58yrr2ujlft9k",
|
|
616
|
+
first_name: "John",
|
|
617
|
+
last_name: "Doe",
|
|
416
618
|
type: "bank_account",
|
|
417
|
-
|
|
619
|
+
country: "NG",
|
|
418
620
|
bvn: "20012345677",
|
|
419
621
|
bank_code: "007",
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
response = paystack.customers.validate(customer_code, validation_params)
|
|
622
|
+
account_number: "0123456789"
|
|
623
|
+
)
|
|
425
624
|
|
|
426
625
|
if response.success?
|
|
427
626
|
puts "Customer validation initiated: #{response.message}"
|
|
@@ -430,15 +629,14 @@ else
|
|
|
430
629
|
end
|
|
431
630
|
```
|
|
432
631
|
|
|
433
|
-
#### Set Risk Action
|
|
632
|
+
#### Set Risk Action (Whitelist/Blacklist)
|
|
434
633
|
|
|
435
634
|
```ruby
|
|
436
|
-
|
|
635
|
+
# customer: the customer code or email address
|
|
636
|
+
response = paystack.customers.set_risk_action(
|
|
437
637
|
customer: "CUS_xr58yrr2ujlft9k",
|
|
438
|
-
risk_action: "allow"
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
response = paystack.customers.set_risk_action(params)
|
|
638
|
+
risk_action: "allow" # "allow" to whitelist, "deny" to blacklist, "default" to reset
|
|
639
|
+
)
|
|
442
640
|
|
|
443
641
|
if response.success?
|
|
444
642
|
puts "Risk action set successfully!"
|
|
@@ -449,14 +647,35 @@ else
|
|
|
449
647
|
end
|
|
450
648
|
```
|
|
451
649
|
|
|
452
|
-
####
|
|
650
|
+
#### Authorizations and Direct Debit
|
|
453
651
|
|
|
454
652
|
```ruby
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
653
|
+
# Start creating a reusable authorization (direct_debit is the only channel for now)
|
|
654
|
+
response = paystack.customers.initialize_authorization(
|
|
655
|
+
email: "customer@example.com",
|
|
656
|
+
channel: "direct_debit",
|
|
657
|
+
callback_url: "https://example.com/callback"
|
|
658
|
+
)
|
|
659
|
+
reference = response.data.reference # send the customer to response.data.redirect_url
|
|
458
660
|
|
|
459
|
-
|
|
661
|
+
# Check the authorization's status
|
|
662
|
+
paystack.customers.verify_authorization(reference: reference)
|
|
663
|
+
|
|
664
|
+
# Link a bank account to an existing customer for Direct Debit (id is the numeric customer ID)
|
|
665
|
+
paystack.customers.initialize_direct_debit(
|
|
666
|
+
id: 12345,
|
|
667
|
+
account: {number: "0123456789", bank_code: "058"},
|
|
668
|
+
address: {street: "Some Where", city: "Ikeja", state: "Lagos"}
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# The customer's Direct Debit mandates
|
|
672
|
+
paystack.customers.fetch_mandate_authorizations(id: 12345)
|
|
673
|
+
|
|
674
|
+
# Trigger an activation charge on an inactive mandate
|
|
675
|
+
paystack.customers.direct_debit_activation_charge(id: 12345, authorization_id: 1069309917)
|
|
676
|
+
|
|
677
|
+
# Deactivate an authorization (any channel)
|
|
678
|
+
response = paystack.customers.deactivate_authorization(authorization_code: "AUTH_72btv547")
|
|
460
679
|
|
|
461
680
|
if response.success?
|
|
462
681
|
puts "Authorization deactivated: #{response.message}"
|
|
@@ -465,6 +684,217 @@ else
|
|
|
465
684
|
end
|
|
466
685
|
```
|
|
467
686
|
|
|
687
|
+
### Banks
|
|
688
|
+
|
|
689
|
+
Everything takes keyword arguments. Account numbers and bank codes are strings, so leading zeros survive.
|
|
690
|
+
|
|
691
|
+
```ruby
|
|
692
|
+
# List banks. Filters are optional: country (ghana, kenya, nigeria, "south africa"), currency,
|
|
693
|
+
# type, gateway, per_page, page, use_cursor, next_cursor, previous, ...
|
|
694
|
+
response = paystack.banks.list(country: "nigeria", per_page: 50)
|
|
695
|
+
response.data.each { |bank| puts "#{bank["name"]}: #{bank["code"]}" }
|
|
696
|
+
|
|
697
|
+
# Resolve an account number to the name on the account
|
|
698
|
+
response = paystack.banks.resolve_account_number(account_number: "0022728151", bank_code: "063")
|
|
699
|
+
puts response.data["account_name"] if response.success?
|
|
700
|
+
|
|
701
|
+
# Validate a South African bank account before sending money
|
|
702
|
+
response = paystack.banks.validate_account(
|
|
703
|
+
account_name: "Ama Mensah",
|
|
704
|
+
account_number: "0123456789",
|
|
705
|
+
account_type: "personal",
|
|
706
|
+
bank_code: "632005",
|
|
707
|
+
country_code: "ZA",
|
|
708
|
+
document_type: "identityNumber",
|
|
709
|
+
document_number: "1234567890123" # optional
|
|
710
|
+
)
|
|
711
|
+
```
|
|
712
|
+
|
|
713
|
+
### Miscellaneous
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
# Details of a card BIN (6 or 8 digits, as a string)
|
|
717
|
+
response = paystack.miscellaneous.resolve_card_bin(bin: "539983")
|
|
718
|
+
puts response.data["bank"] if response.success?
|
|
719
|
+
|
|
720
|
+
# Supported countries
|
|
721
|
+
paystack.miscellaneous.list_countries
|
|
722
|
+
|
|
723
|
+
# States for address verification (country is required)
|
|
724
|
+
paystack.miscellaneous.list_states(country: "CA")
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Transfer Recipients
|
|
728
|
+
|
|
729
|
+
Every method takes keyword arguments. A recipient is who you send a transfer to.
|
|
730
|
+
|
|
731
|
+
#### Create a Transfer Recipient
|
|
732
|
+
|
|
733
|
+
```ruby
|
|
734
|
+
response = paystack.transfer_recipients.create(
|
|
735
|
+
type: "nuban", # nuban, ghipss, mobile_money, basa or authorization
|
|
736
|
+
name: "Ama Mensah",
|
|
737
|
+
account_number: "0123456789",
|
|
738
|
+
bank_code: "058",
|
|
739
|
+
currency: "NGN",
|
|
740
|
+
metadata: {job: "Baker"}
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
puts response.data.recipient_code if response.success?
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
A duplicate account number returns the existing recipient. To create many at once, pass a list of recipient hashes:
|
|
747
|
+
|
|
748
|
+
```ruby
|
|
749
|
+
response = paystack.transfer_recipients.bulk_create(
|
|
750
|
+
batch: [
|
|
751
|
+
{type: "nuban", name: "Ama Mensah", account_number: "0123456789", bank_code: "058"},
|
|
752
|
+
{type: "nuban", name: "Kofi Boateng", account_number: "0987654321", bank_code: "058"}
|
|
753
|
+
]
|
|
754
|
+
)
|
|
755
|
+
|
|
756
|
+
response.data.success # recipients that were created
|
|
757
|
+
response.data.errors # records Paystack rejected, with the reason
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
#### List, Fetch, Update and Delete
|
|
761
|
+
|
|
762
|
+
```ruby
|
|
763
|
+
paystack.transfer_recipients.list(per_page: 20, page: 2)
|
|
764
|
+
|
|
765
|
+
# fetch, update and delete take the recipient code or the numeric ID
|
|
766
|
+
paystack.transfer_recipients.fetch(id_or_code: "RCP_2x5j67tnnw1t98k")
|
|
767
|
+
paystack.transfer_recipients.update(id_or_code: "RCP_2x5j67tnnw1t98k", name: "Ama K. Mensah", email: "ama@example.com")
|
|
768
|
+
paystack.transfer_recipients.delete(id_or_code: "RCP_2x5j67tnnw1t98k") # Paystack sets the recipient to inactive
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
Paystack's docs list `from` and `to` filters on the list endpoint, but the API ignores them (checked against the test API), so the SDK does not offer them. Cursor pagination is available with `use_cursor: true`, `next_cursor:` and `previous:`.
|
|
772
|
+
|
|
773
|
+
### Transfers
|
|
774
|
+
|
|
775
|
+
Transfers send money from your Paystack balance to a transfer recipient (`RCP_...`). They move real money in live mode, so the SDK never retries them except on `429` (see [Timeouts and Retries](#timeouts-and-retries)).
|
|
776
|
+
|
|
777
|
+
#### Initiate a Transfer
|
|
778
|
+
|
|
779
|
+
```ruby
|
|
780
|
+
# Amount is in the smallest currency unit (kobo, pesewas, cents).
|
|
781
|
+
# reference is required: generate it once and reuse it if you retry, so a retry cannot pay twice.
|
|
782
|
+
# Paystack documents 16 to 50 characters of lowercase letters, digits, - and _.
|
|
783
|
+
response = paystack.transfers.create(
|
|
784
|
+
source: "balance",
|
|
785
|
+
amount: 100_000,
|
|
786
|
+
recipient: "RCP_gd9vgag7n5lr5ix",
|
|
787
|
+
reference: "acv_9ee55786-2323-4760-98e2-6380c9cb3f68",
|
|
788
|
+
reason: "Bonus for the week"
|
|
789
|
+
)
|
|
790
|
+
|
|
791
|
+
if response.success?
|
|
792
|
+
case response.data.status
|
|
793
|
+
when "otp"
|
|
794
|
+
# Your integration requires an OTP: Paystack sent one to the business phone
|
|
795
|
+
paystack.transfers.finalize(transfer_code: response.data.transfer_code, otp: "928783")
|
|
796
|
+
else
|
|
797
|
+
puts "Transfer #{response.data.transfer_code} is #{response.data.status}"
|
|
798
|
+
end
|
|
799
|
+
else
|
|
800
|
+
puts "Error: #{response.error_message}"
|
|
801
|
+
end
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
#### Verify, Fetch and List Transfers
|
|
805
|
+
|
|
806
|
+
```ruby
|
|
807
|
+
# Verify by your reference
|
|
808
|
+
paystack.transfers.verify(reference: "acv_9ee55786-2323-4760-98e2-6380c9cb3f68")
|
|
809
|
+
|
|
810
|
+
# Fetch by transfer ID or code
|
|
811
|
+
paystack.transfers.fetch(id_or_code: "TRF_v5tip3zx8nna9o78")
|
|
812
|
+
|
|
813
|
+
# List, with Paystack's page pagination (default 50 per page)...
|
|
814
|
+
paystack.transfers.list(per_page: 20, page: 2, from: "2025-01-01", to: "2025-04-30")
|
|
815
|
+
|
|
816
|
+
# ...filtered by the numeric recipient ID, or by status
|
|
817
|
+
paystack.transfers.list(recipient: 56824902, status: "success")
|
|
818
|
+
|
|
819
|
+
# ...or with cursor pagination: pass response.meta.next back as next_cursor
|
|
820
|
+
paystack.transfers.list(use_cursor: true, per_page: 20)
|
|
821
|
+
|
|
822
|
+
# Export transfers (in Paystack's OpenAPI spec, not on its docs page)
|
|
823
|
+
paystack.transfers.export(from: "2025-01-01", to: "2025-04-30", status: "success")
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
#### Bulk Transfers and the OTP Requirement
|
|
827
|
+
|
|
828
|
+
```ruby
|
|
829
|
+
# Bulk transfers need the OTP requirement disabled. Each transfer uses Paystack's field names.
|
|
830
|
+
paystack.transfers.bulk_create(
|
|
831
|
+
source: "balance",
|
|
832
|
+
currency: "NGN",
|
|
833
|
+
transfers: [
|
|
834
|
+
{amount: 20_000, recipient: "RCP_gd9vgag7n5lr5ix", reference: "acv_2627bbfe-1a2a-4a1a-8d0e-9d2ee6c31496", reason: "Bonus"},
|
|
835
|
+
{amount: 35_000, recipient: "RCP_zpk2tgagu6lgb4g", reference: "acv_1bd0c1f8-78c2-463b-8bd4-ed9eeb36be50", reason: "Bonus"}
|
|
836
|
+
]
|
|
837
|
+
)
|
|
838
|
+
|
|
839
|
+
# Turn the OTP requirement off (Paystack sends an OTP to the business phone), then confirm it
|
|
840
|
+
paystack.transfers.disable_otp
|
|
841
|
+
paystack.transfers.finalize_disable_otp(otp: "928783")
|
|
842
|
+
|
|
843
|
+
# Turn it back on
|
|
844
|
+
paystack.transfers.enable_otp
|
|
845
|
+
|
|
846
|
+
# Resend the OTP for a transfer awaiting one
|
|
847
|
+
paystack.transfers.resend_otp(transfer_code: "TRF_vsyqdmlzble3uii", reason: "resend_otp")
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
### Refunds
|
|
851
|
+
|
|
852
|
+
Refunds return money from a successful transaction to the customer. They move real money in live mode, so the SDK never retries them except on `429` (see [Timeouts and Retries](#timeouts-and-retries)).
|
|
853
|
+
|
|
854
|
+
#### Create a Refund
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
# transaction is the transaction's reference or its numeric ID.
|
|
858
|
+
# amount is in the smallest currency unit (kobo, pesewas, cents) and cannot exceed the transaction amount.
|
|
859
|
+
# Leave amount out to refund the whole transaction; pass less for a partial refund.
|
|
860
|
+
response = paystack.refunds.create(
|
|
861
|
+
transaction: "T685312322670591",
|
|
862
|
+
amount: 2_500,
|
|
863
|
+
currency: "GHS", # optional; Paystack refuses one that differs from the transaction's
|
|
864
|
+
customer_note: "Duplicate payment",
|
|
865
|
+
merchant_note: "Refunded by the finance team"
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
if response.success?
|
|
869
|
+
puts "Refund #{response.data.id} is #{response.data.status}" # "pending" until Paystack processes it
|
|
870
|
+
else
|
|
871
|
+
puts "Error: #{response.error_message}" # e.g. "Transaction not found"
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# A full refund by transaction ID
|
|
875
|
+
paystack.refunds.create(transaction: 1_004_723_697)
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
#### Fetch, List and Retry Refunds
|
|
879
|
+
|
|
880
|
+
```ruby
|
|
881
|
+
paystack.refunds.fetch(id: 18_625_648)
|
|
882
|
+
|
|
883
|
+
# List, with page pagination (default 50 per page) and a date range...
|
|
884
|
+
paystack.refunds.list(per_page: 20, page: 1, from: "2025-01-01", to: "2025-04-30")
|
|
885
|
+
|
|
886
|
+
# ...or the refunds of one transaction, by its numeric ID (a reference returns none)
|
|
887
|
+
paystack.refunds.list(transaction_id: 1_004_723_697)
|
|
888
|
+
|
|
889
|
+
# Retry a refund with the needs-attention status by giving the customer's bank account
|
|
890
|
+
paystack.refunds.retry_with_customer_details(
|
|
891
|
+
id: 18_625_648,
|
|
892
|
+
refund_account_details: {currency: "GHS", account_number: "0123456789", bank_id: "9"}
|
|
893
|
+
)
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
Paystack's docs also list a `currency` filter on the list endpoint, but the test API returned the same refunds for every currency, so the SDK does not offer it.
|
|
897
|
+
|
|
468
898
|
### Response Handling
|
|
469
899
|
|
|
470
900
|
#### Working with Response Objects
|
|
@@ -472,7 +902,7 @@ end
|
|
|
472
902
|
All API requests return a `PaystackSdk::Response` object that provides easy access to the response data.
|
|
473
903
|
|
|
474
904
|
```ruby
|
|
475
|
-
response = paystack.transactions.initiate(params)
|
|
905
|
+
response = paystack.transactions.initiate(**params)
|
|
476
906
|
|
|
477
907
|
# Check if the request was successful
|
|
478
908
|
response.success? # => true or false
|
|
@@ -490,6 +920,12 @@ response.authorization_url # Same as response.data.authorization_url
|
|
|
490
920
|
# Access nested data
|
|
491
921
|
response.data.customer.email
|
|
492
922
|
|
|
923
|
+
# Hash-style access works with strings or symbols
|
|
924
|
+
response[:reference]
|
|
925
|
+
response["reference"]
|
|
926
|
+
response[:customer][:email]
|
|
927
|
+
response.key?(:reference) # => true
|
|
928
|
+
|
|
493
929
|
# For arrays, use array methods
|
|
494
930
|
response.data.first # First item in an array
|
|
495
931
|
response.data.last # Last item in an array
|
|
@@ -501,6 +937,21 @@ response.data.each do |item|
|
|
|
501
937
|
end
|
|
502
938
|
```
|
|
503
939
|
|
|
940
|
+
#### Pagination Metadata
|
|
941
|
+
|
|
942
|
+
List responses carry Paystack's pagination details in `meta`:
|
|
943
|
+
|
|
944
|
+
```ruby
|
|
945
|
+
response = paystack.transactions.list(per_page: 20)
|
|
946
|
+
|
|
947
|
+
response.meta.total # => 40
|
|
948
|
+
response.meta.page # => 1
|
|
949
|
+
response.meta.pageCount # => 2
|
|
950
|
+
response.meta.perPage # => 20
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
`response.meta` is `nil` when the response has no `meta`.
|
|
954
|
+
|
|
504
955
|
#### Accessing the Original Response
|
|
505
956
|
|
|
506
957
|
Sometimes you may need access to the original API response:
|
|
@@ -511,9 +962,8 @@ response = paystack.transactions.list
|
|
|
511
962
|
# Access the original response body
|
|
512
963
|
original = response.original_response
|
|
513
964
|
|
|
514
|
-
#
|
|
515
|
-
|
|
516
|
-
current_page = original.dig("meta", "page")
|
|
965
|
+
# Anything else in the body is still reachable
|
|
966
|
+
original.dig("meta", "total")
|
|
517
967
|
```
|
|
518
968
|
|
|
519
969
|
#### Exception Handling
|
|
@@ -526,9 +976,14 @@ begin
|
|
|
526
976
|
rescue PaystackSdk::AuthenticationError => e
|
|
527
977
|
puts "Authentication failed: #{e.message}"
|
|
528
978
|
rescue PaystackSdk::RateLimitError => e
|
|
529
|
-
|
|
979
|
+
# retry_after is nil when Paystack did not send x-ratelimit-reset
|
|
980
|
+
puts "Rate limit exceeded. Retry after: #{e.retry_after || "unknown"} seconds"
|
|
530
981
|
rescue PaystackSdk::ServerError => e
|
|
531
982
|
puts "Server error: #{e.message}"
|
|
983
|
+
rescue PaystackSdk::TimeoutError, PaystackSdk::ConnectionError => e
|
|
984
|
+
# For writes (e.g. creating a transfer) the request may still have been processed:
|
|
985
|
+
# verify by your own reference before retrying
|
|
986
|
+
puts "Could not complete the request: #{e.message}"
|
|
532
987
|
rescue PaystackSdk::APIError => e
|
|
533
988
|
puts "API error: #{e.message}"
|
|
534
989
|
rescue PaystackSdk::Error => e
|
|
@@ -566,36 +1021,39 @@ The SDK includes several specific error classes:
|
|
|
566
1021
|
- **`PaystackSdk::RateLimitError`** - Rate limiting encountered
|
|
567
1022
|
- **`PaystackSdk::ServerError`** - Server errors (5xx responses)
|
|
568
1023
|
|
|
1024
|
+
- **`PaystackSdk::ConnectionError`** - Could not reach Paystack (DNS, refused connection, TLS) after retries
|
|
1025
|
+
- **`PaystackSdk::TimeoutError`** - The request timed out after retries
|
|
1026
|
+
|
|
569
1027
|
##### Validation Error Examples
|
|
570
1028
|
|
|
571
1029
|
The SDK validates your input data **before** making API calls and will throw exceptions immediately if required data is missing or incorrectly formatted:
|
|
572
1030
|
|
|
573
1031
|
```ruby
|
|
574
|
-
# Missing required
|
|
1032
|
+
# Missing required keyword
|
|
575
1033
|
begin
|
|
576
|
-
paystack.transactions.initiate(
|
|
577
|
-
rescue
|
|
578
|
-
puts e.message # => "
|
|
1034
|
+
paystack.transactions.initiate(amount: 1000) # Missing email
|
|
1035
|
+
rescue ArgumentError => e
|
|
1036
|
+
puts e.message # => "missing keyword: :email"
|
|
579
1037
|
end
|
|
580
1038
|
|
|
581
1039
|
# Invalid format
|
|
582
1040
|
begin
|
|
583
|
-
paystack.transactions.initiate(
|
|
1041
|
+
paystack.transactions.initiate(
|
|
584
1042
|
email: "invalid-email", # Not a valid email format
|
|
585
1043
|
amount: 1000
|
|
586
|
-
|
|
1044
|
+
)
|
|
587
1045
|
rescue PaystackSdk::InvalidFormatError => e
|
|
588
1046
|
puts e.message # => "Invalid format for Email. Expected format: valid email address"
|
|
589
1047
|
end
|
|
590
1048
|
|
|
591
1049
|
# Invalid value
|
|
592
1050
|
begin
|
|
593
|
-
paystack.customers.set_risk_action(
|
|
1051
|
+
paystack.customers.set_risk_action(
|
|
594
1052
|
customer: "CUS_123",
|
|
595
1053
|
risk_action: "invalid_action" # Not in allowed values
|
|
596
|
-
|
|
1054
|
+
)
|
|
597
1055
|
rescue PaystackSdk::InvalidValueError => e
|
|
598
|
-
puts e.message # => "Invalid value for risk_action: must be one of
|
|
1056
|
+
puts e.message # => "Invalid value for risk_action: must be one of: allow, deny, default"
|
|
599
1057
|
end
|
|
600
1058
|
```
|
|
601
1059
|
|
|
@@ -603,6 +1061,68 @@ These validation errors are thrown immediately and prevent the API call from bei
|
|
|
603
1061
|
|
|
604
1062
|
## Advanced Usage
|
|
605
1063
|
|
|
1064
|
+
### Timeouts and Retries
|
|
1065
|
+
|
|
1066
|
+
Connections built by the SDK time out and retry transient failures by default:
|
|
1067
|
+
|
|
1068
|
+
```ruby
|
|
1069
|
+
client = PaystackSdk::Client.new(
|
|
1070
|
+
secret_key: "sk_test_xxx",
|
|
1071
|
+
timeout: 30, # seconds to wait for a response
|
|
1072
|
+
open_timeout: 5, # seconds to wait for the connection to open
|
|
1073
|
+
max_retries: 2, # retries after the first attempt (0 disables retrying)
|
|
1074
|
+
retry_interval: 0.5 # base backoff in seconds, doubled on each retry with jitter
|
|
1075
|
+
)
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
- Read-only requests (`GET`) are retried on network failures and `429`/`502`/`503`/`504` responses.
|
|
1079
|
+
- Anything that writes (`POST`, `PUT`, `DELETE`) is retried **only on `429`**, where Paystack rejected the
|
|
1080
|
+
request for exceeding the rate limit. A timeout or `5xx` on a write may mean Paystack processed it, so it is
|
|
1081
|
+
not retried. Pass `retry_non_idempotent: true` only if you deduplicate yourself (e.g. verify by reference first).
|
|
1082
|
+
- On a `429` the SDK waits for Paystack's `x-ratelimit-reset` header (seconds) before retrying. If the wait
|
|
1083
|
+
exceeds 10 seconds it stops retrying and raises `PaystackSdk::RateLimitError` (`#retry_after` holds the value).
|
|
1084
|
+
- Errors that survive all retries are raised as `PaystackSdk::RateLimitError`, `PaystackSdk::ServerError`,
|
|
1085
|
+
`PaystackSdk::TimeoutError` or `PaystackSdk::ConnectionError`, all of which inherit from `PaystackSdk::Error`.
|
|
1086
|
+
- These options only apply to connections the SDK creates. Passing them together with your own `Faraday::Connection` raises `ArgumentError`; configure your connection yourself. Invalid values (e.g. a negative `max_retries`) also raise `ArgumentError`.
|
|
1087
|
+
|
|
1088
|
+
### Webhooks
|
|
1089
|
+
|
|
1090
|
+
Paystack tells you about changes (a charge succeeded, a refund was processed, a transfer failed) by POSTing events to your webhook URL. `PaystackSdk::Webhook` verifies and parses them, and works with any framework.
|
|
1091
|
+
|
|
1092
|
+
Paystack signs each event: the `x-paystack-signature` header is a lowercase hex HMAC SHA512 of the **raw request body**, using your secret key. Verify it before using the event.
|
|
1093
|
+
|
|
1094
|
+
```ruby
|
|
1095
|
+
# Rails controller (skip CSRF protection for this action)
|
|
1096
|
+
def create
|
|
1097
|
+
event = PaystackSdk::Webhook.construct_event(
|
|
1098
|
+
payload: request.raw_post, # raw body, not params
|
|
1099
|
+
signature: request.headers["X-Paystack-Signature"],
|
|
1100
|
+
secret: ENV.fetch("PAYSTACK_SECRET_KEY")
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
HandlePaystackEventJob.perform_later(event.payload) # do the work in the background
|
|
1104
|
+
head :ok # acknowledge quickly
|
|
1105
|
+
rescue PaystackSdk::WebhookError
|
|
1106
|
+
head :bad_request
|
|
1107
|
+
end
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
```ruby
|
|
1111
|
+
event.event # => "charge.success"
|
|
1112
|
+
event.data.reference # data is wrapped like any Response
|
|
1113
|
+
event.data[:amount]
|
|
1114
|
+
event.known? # => true if Paystack documents this event name
|
|
1115
|
+
event.payload # the parsed JSON Hash (string keys), handy for queuing a job
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
- `Webhook.valid_signature?(payload:, signature:, secret:)` returns a boolean, and `Webhook.verify!` raises `PaystackSdk::InvalidSignatureError`. The comparison is constant-time.
|
|
1119
|
+
- The payload must be the exact bytes received. A parsed or re-serialised body will not match, and a non-String payload raises `ArgumentError`. A blank secret also raises, rather than checking against nothing.
|
|
1120
|
+
- `Webhook.construct_event` verifies first and parses only afterwards. A signed body that is not a JSON event raises `PaystackSdk::InvalidPayloadError`. Events Paystack adds later still come back, with `known?` false.
|
|
1121
|
+
- `Webhook.trusted_ip?(ip)` checks the three addresses Paystack documents (`Webhook::IP_ADDRESSES`). Use it as an extra check, not instead of the signature.
|
|
1122
|
+
- `Webhook.sign(payload, secret)` produces a valid signature, for testing your own endpoint.
|
|
1123
|
+
- Paystack retries events your server does not acknowledge with a `200 OK`: in live mode every 3 minutes for 4 tries, then hourly for 72 hours. Return `200` fast, process in a background job, and make handlers safe to run more than once.
|
|
1124
|
+
- Before giving value for a `charge.success`, confirm it with `transactions.verify(reference:)`, as Paystack recommends.
|
|
1125
|
+
|
|
606
1126
|
### Environment Variables
|
|
607
1127
|
|
|
608
1128
|
You can use environment variables to configure the SDK:
|
|
@@ -641,11 +1161,29 @@ For more detailed documentation on specific resources, please refer to the follo
|
|
|
641
1161
|
- [Customers](https://paystack.com/docs/api/customer/)
|
|
642
1162
|
- [Plans](https://paystack.com/docs/api/plan/)
|
|
643
1163
|
- [Subscriptions](https://paystack.com/docs/api/subscription/)
|
|
1164
|
+
- [Payment Channels: Mobile Money](https://paystack.com/docs/payments/payment-channels/#mobile-money)
|
|
644
1165
|
|
|
645
1166
|
## Development
|
|
646
1167
|
|
|
647
1168
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
648
1169
|
|
|
1170
|
+
### Style and Linting
|
|
1171
|
+
|
|
1172
|
+
This project uses [StandardRB](https://github.com/standardrb/standard) for code style and linting.
|
|
1173
|
+
|
|
1174
|
+
Add to your Gemfile (if not already present):
|
|
1175
|
+
|
|
1176
|
+
```ruby
|
|
1177
|
+
gem "standard"
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
- Lint: `bundle exec standardrb`
|
|
1181
|
+
- Auto-fix: `bundle exec standardrb --fix`
|
|
1182
|
+
- Via Rake: `bundle exec rake standard`
|
|
1183
|
+
- Default task (runs specs + standard): `bundle exec rake`
|
|
1184
|
+
|
|
1185
|
+
If you encounter cache permission issues locally, you can disable caching: `bundle exec standardrb --no-cache`.
|
|
1186
|
+
|
|
649
1187
|
### Testing
|
|
650
1188
|
|
|
651
1189
|
The SDK includes comprehensive test coverage with consistent response format handling. All test specifications use string keys with hashrocket notation (`=>`) to match the actual format returned by the Paystack API:
|
|
@@ -667,7 +1205,7 @@ Tests also validate specific error types to ensure proper exception handling:
|
|
|
667
1205
|
|
|
668
1206
|
```ruby
|
|
669
1207
|
# Testing specific error types
|
|
670
|
-
expect { customers.set_risk_action(
|
|
1208
|
+
expect { customers.set_risk_action(customer: "CUS_123", risk_action: "block") }
|
|
671
1209
|
.to raise_error(PaystackSdk::InvalidValueError, /risk_action/i)
|
|
672
1210
|
```
|
|
673
1211
|
|