paystack_sdk 0.2.0 → 0.3.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 +20 -0
- data/README.md +469 -0
- data/lib/paystack_sdk/client.rb +81 -0
- data/lib/paystack_sdk/resources/balances.rb +56 -0
- data/lib/paystack_sdk/resources/customers.rb +2 -2
- data/lib/paystack_sdk/resources/dedicated_virtual_accounts.rb +241 -0
- data/lib/paystack_sdk/resources/disputes.rb +237 -0
- data/lib/paystack_sdk/resources/payment_requests.rb +285 -0
- data/lib/paystack_sdk/resources/plans.rb +188 -0
- data/lib/paystack_sdk/resources/settlements.rb +51 -0
- data/lib/paystack_sdk/resources/splits.rb +186 -0
- data/lib/paystack_sdk/resources/subaccounts.rb +156 -0
- data/lib/paystack_sdk/resources/subscriptions.rb +161 -0
- data/lib/paystack_sdk/version.rb +1 -1
- metadata +10 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8893378c4721f9f126d73b888dd539060c93058488b49c92833183b819d06023
|
|
4
|
+
data.tar.gz: a787c8b3b413e4171c3574e38dc519c0b2eed3df09776b383a0b20fc259bd75a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a205e604c23fc3ca07a1ad7096ef920c9587fc04d322da9c40e6e00bd92870c2d833f554b275c6651a3f1aa07c23d56e2cb77fc41d2a02005e5848f8686942a4
|
|
7
|
+
data.tar.gz: d5cbb5b6bfefe710024e1cba4dcaa2097577b4df3f17e001199b281945cb8d417000c89a0fd5292d8ca8444272d55313f010b2acfadc130246372433f0465166
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0] - 2026-10-09
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `client.splits` for Paystack's Transaction Splits API: `create`, `list`, `fetch`, `update`, `add_subaccount` (also updates an existing subaccount's share) and `remove_subaccount`, generated from the OpenAPI spec. `list` sends `perPage` (the API ignores the spec's `per_page`); `fetch` takes a split code or numeric ID; `add_subaccount` requires `subaccount` and `share`, and `remove_subaccount` requires `subaccount`, as the docs and test API do.
|
|
10
|
+
- `disputes` (new, generated from Paystack's OpenAPI spec): `list`, `fetch`, `list_transaction`, `update`, `add_evidence`, `fetch_upload_url`, `resolve` and `export`. `fetch_upload_url` takes `upload_filename`, which Paystack documents and the test API honours although the OpenAPI spec omits it.
|
|
11
|
+
- `Subaccounts` (`client.subaccounts`): `create`, `list`, `fetch(id_or_code:)` and `update(id_or_code:, ...)`, generated from Paystack's OpenAPI spec. The bank is sent as `bank_code`, as Paystack's docs name it (the spec says `settlement_bank`; the test API reads both and prefers `bank_code`). `list(active:)` takes `1` or `0`: Paystack reads `true` as inactive.
|
|
12
|
+
- `Plans` (`client.plans`): `create`, `list`, `fetch(id_or_code:)` and `update(id_or_code:, ...)`, generated from Paystack's OpenAPI spec. `interval` accepts `hourly` and `quarterly` as well as the spec's five values (the test API accepts all seven; the spec omits two). `update` takes `update_existing_subscriptions`, which Paystack documents and the test API validates although the spec omits it; leave it out and Paystack applies the change to existing subscriptions. `list(status:)` is documented by Paystack but absent from the spec.
|
|
13
|
+
- `settlements` (new): `list(per_page:, page:, from:, to:)` for `GET /settlement` and `transactions(id:)` for `GET /settlement/{id}/transactions`. `from` and `to` are in Paystack's docs but not the spec; the test API validates them.
|
|
14
|
+
- `Balances` (`client.balances`): `fetch` and `ledger(per_page:, page:, from:, to:)`, generated from Paystack's OpenAPI spec and read-only (GET). `fetch` returns one entry per currency with the balance in the currency's subunit (pesewas for GHS). Paystack's docs have no Balance page; the two operations sit on the Transfers Control page and the behaviour was confirmed against the test API.
|
|
15
|
+
- `Subscriptions` (`client.subscriptions`): `create`, `list`, `fetch(id_or_code:)`, `enable(code:, token:)`, `disable(code:, token:)`, `generate_update_link(code:)` and `send_update_link(code:)`, generated from Paystack's OpenAPI spec. `list` filters by `plan_id:` and `customer_id:` (sent as `plan` and `customer`): the test API matches numeric IDs only. `start_date` is checked as ISO 8601 before sending, since the test API refuses an invalid one but still creates the subscription.
|
|
16
|
+
- `DedicatedVirtualAccounts` (`client.dedicated_virtual_accounts`), generated from Paystack's OpenAPI spec: `create`, `assign`, `list`, `fetch(dedicated_account_id:)`, `requery`, `deactivate(dedicated_account_id:)`, `add_split`, `remove_split` and `fetch_bank_providers`.
|
|
17
|
+
- `PaymentRequests` (`client.payment_requests`), generated from Paystack's OpenAPI spec: `create`, `list`, `fetch(id_or_code:)`, `update(id_or_code:)`, `verify(code:)`, `notify(code:)`, `totals`, `finalize(id_or_code:, send_notification: nil)` and `archive(id_or_code:)`. Checked against the Paystack test API, `create` does not require `amount` when `line_items` or `tax` are given (the spec says it is required), takes `metadata` as a Hash and `redirect_url` (both docs-only), and accepts a date as `due_date`; `list` filters by the numeric `customer_id:` and takes `include_archive:`.
|
|
18
|
+
- `bin/paystack-scaffold` applies `required: true` from body-field entries in `spec/support/paystack_contract_exceptions.yml`, as it already did for query parameters.
|
|
19
|
+
- `bin/paystack-scaffold` applies `enum:` from body-field entries in `spec/support/paystack_contract_exceptions.yml`, as it already did for query parameters.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `bin/paystack-scaffold` now adds a resource's `Client` accessor above `Client`'s private helpers, where it is public, instead of at the end of the class. It also finds the docs anchor of an operation whose cURL sample on the docs page calls another path (Split Dedicated Account Transaction), and corrects the `@see` link of `customers.direct_debit_activation_charge` to `#directdebit-activation-charge`.
|
|
24
|
+
|
|
5
25
|
## [0.2.0] - 2026-10-09
|
|
6
26
|
|
|
7
27
|
### Breaking
|
data/README.md
CHANGED
|
@@ -41,6 +41,36 @@ The `paystack_sdk` gem provides a simple and intuitive interface for interacting
|
|
|
41
41
|
- [Refunds](#refunds)
|
|
42
42
|
- [Create a Refund](#create-a-refund)
|
|
43
43
|
- [Fetch, List and Retry Refunds](#fetch-list-and-retry-refunds)
|
|
44
|
+
- [Settlements](#settlements)
|
|
45
|
+
- [Splits](#splits)
|
|
46
|
+
- [Create a Split](#create-a-split)
|
|
47
|
+
- [List, Fetch and Update Splits](#list-fetch-and-update-splits)
|
|
48
|
+
- [Add, Update or Remove a Subaccount](#add-update-or-remove-a-subaccount)
|
|
49
|
+
- [Disputes](#disputes)
|
|
50
|
+
- [List and Fetch Disputes](#list-and-fetch-disputes)
|
|
51
|
+
- [Answer a Dispute](#answer-a-dispute)
|
|
52
|
+
- [Subaccounts](#subaccounts)
|
|
53
|
+
- [Create a Subaccount](#create-a-subaccount)
|
|
54
|
+
- [List and Fetch Subaccounts](#list-and-fetch-subaccounts)
|
|
55
|
+
- [Update a Subaccount](#update-a-subaccount)
|
|
56
|
+
- [Balances](#balances)
|
|
57
|
+
- [Plans](#plans)
|
|
58
|
+
- [Create a Plan](#create-a-plan)
|
|
59
|
+
- [List and Fetch Plans](#list-and-fetch-plans)
|
|
60
|
+
- [Update a Plan](#update-a-plan)
|
|
61
|
+
- [Subscriptions](#subscriptions)
|
|
62
|
+
- [Create a Subscription](#create-a-subscription)
|
|
63
|
+
- [List and Fetch Subscriptions](#list-and-fetch-subscriptions)
|
|
64
|
+
- [Enable or Disable a Subscription](#enable-or-disable-a-subscription)
|
|
65
|
+
- [Let the Customer Update Their Card](#let-the-customer-update-their-card)
|
|
66
|
+
- [Dedicated Virtual Accounts](#dedicated-virtual-accounts)
|
|
67
|
+
- [Create or Assign an Account](#create-or-assign-an-account)
|
|
68
|
+
- [List, Fetch, Requery and Deactivate](#list-fetch-requery-and-deactivate)
|
|
69
|
+
- [Split Payments on an Account](#split-payments-on-an-account)
|
|
70
|
+
- [Payment Requests](#payment-requests)
|
|
71
|
+
- [Create a Payment Request](#create-a-payment-request)
|
|
72
|
+
- [List, Fetch and Verify Payment Requests](#list-fetch-and-verify-payment-requests)
|
|
73
|
+
- [Update, Finalize, Notify and Archive](#update-finalize-notify-and-archive)
|
|
44
74
|
- [Response Handling](#response-handling)
|
|
45
75
|
- [Working with Response Objects](#working-with-response-objects)
|
|
46
76
|
- [Accessing the Original Response](#accessing-the-original-response)
|
|
@@ -894,6 +924,445 @@ paystack.refunds.retry_with_customer_details(
|
|
|
894
924
|
```
|
|
895
925
|
|
|
896
926
|
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.
|
|
927
|
+
### Settlements
|
|
928
|
+
|
|
929
|
+
Settlements are the payouts Paystack makes to your bank account. Use them to reconcile what was paid out against the transactions behind each payout.
|
|
930
|
+
|
|
931
|
+
```ruby
|
|
932
|
+
# List settlements (Paystack's defaults apply: 50 per page, page 1)
|
|
933
|
+
paystack.settlements.list
|
|
934
|
+
paystack.settlements.list(per_page: 20, page: 2)
|
|
935
|
+
|
|
936
|
+
# Only settlements within a date range (a date or an ISO 8601 timestamp)
|
|
937
|
+
paystack.settlements.list(from: "2025-01-01", to: "2025-01-31T23:59:59Z")
|
|
938
|
+
|
|
939
|
+
# The transactions that make up one settlement
|
|
940
|
+
paystack.settlements.transactions(id: 3090024)
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Paystack's docs also list `status` and `subaccount` filters on the list endpoint, but they are not in its OpenAPI spec and the SDK does not offer them yet: the test account has no settlements, so whether they filter could not be checked. `transactions` takes no filters or pagination for the same reason.
|
|
944
|
+
|
|
945
|
+
### Splits
|
|
946
|
+
|
|
947
|
+
A transaction split shares the settlement for a payment between your payout account and one or more subaccounts (`ACCT_...`). Create the split once, then pass its `split_code` when you initialize a transaction or create a charge.
|
|
948
|
+
|
|
949
|
+
#### Create a Split
|
|
950
|
+
|
|
951
|
+
```ruby
|
|
952
|
+
# type is "percentage" (each share is a percentage) or "flat" (each share is an amount in the
|
|
953
|
+
# currency's subunit: kobo, pesewas, cents). currency must be enabled on your integration.
|
|
954
|
+
response = paystack.splits.create(
|
|
955
|
+
name: "Halfsies",
|
|
956
|
+
type: "percentage",
|
|
957
|
+
currency: "GHS",
|
|
958
|
+
subaccounts: [{subaccount: "ACCT_6uujpqtzmnufzkw", share: 50}],
|
|
959
|
+
# Optional: who pays Paystack's fee. One of "account", "subaccount", "all-proportional" or "all"
|
|
960
|
+
# (Paystack's default). "subaccount" also needs bearer_subaccount, a subaccount in the split.
|
|
961
|
+
bearer_type: "all-proportional"
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
split_code = response.data.split_code if response.success? # e.g. "SPL_RcScyW5jp2"
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
#### List, Fetch and Update Splits
|
|
968
|
+
|
|
969
|
+
```ruby
|
|
970
|
+
# List, with Paystack's page pagination (default 50 per page) and filters
|
|
971
|
+
paystack.splits.list(per_page: 20, page: 1, active: true)
|
|
972
|
+
paystack.splits.list(name: "Halfsies", subaccount_code: "ACCT_6uujpqtzmnufzkw", from: "2025-01-01", to: "2025-04-30")
|
|
973
|
+
|
|
974
|
+
# Fetch by numeric ID or by split code
|
|
975
|
+
paystack.splits.fetch(id: 2703655)
|
|
976
|
+
paystack.splits.fetch(id: "SPL_RcScyW5jp2")
|
|
977
|
+
|
|
978
|
+
# Update (by numeric ID only: Paystack refuses a split code here)
|
|
979
|
+
paystack.splits.update(id: 2703655, active: false)
|
|
980
|
+
paystack.splits.update(id: 2703655, bearer_type: "subaccount", bearer_subaccount: "ACCT_6uujpqtzmnufzkw")
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### Add, Update or Remove a Subaccount
|
|
984
|
+
|
|
985
|
+
```ruby
|
|
986
|
+
# Adds the subaccount, or changes its share if it is already in the split (numeric split ID)
|
|
987
|
+
paystack.splits.add_subaccount(id: 2703655, subaccount: "ACCT_eg4sob4590pq9vb", share: 20)
|
|
988
|
+
|
|
989
|
+
paystack.splits.remove_subaccount(id: 2703655, subaccount: "ACCT_eg4sob4590pq9vb")
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
### Disputes
|
|
993
|
+
|
|
994
|
+
Disputes (chargebacks) are filed against your transactions by customers or their banks. Listing and fetching are read-only. `update`, `add_evidence` and `resolve` change a real dispute, so call them only when you mean to answer it.
|
|
995
|
+
|
|
996
|
+
#### List and Fetch Disputes
|
|
997
|
+
|
|
998
|
+
```ruby
|
|
999
|
+
# List, with Paystack's page pagination; filter by status, transaction ID or date range.
|
|
1000
|
+
# status is one of awaiting-merchant-feedback, awaiting-bank-feedback, pending, resolved
|
|
1001
|
+
response = paystack.disputes.list(status: "awaiting-merchant-feedback", per_page: 20, from: "2025-01-01", to: "2025-04-30")
|
|
1002
|
+
response.data.each { |dispute| puts "#{dispute.id}: #{dispute.status}" } if response.success?
|
|
1003
|
+
|
|
1004
|
+
# Fetch one dispute, or every dispute filed for a transaction
|
|
1005
|
+
paystack.disputes.fetch(id: 2867)
|
|
1006
|
+
paystack.disputes.list_transaction(id: 5991760)
|
|
1007
|
+
|
|
1008
|
+
# Export disputes. Paystack answers 404 "No disputes found" when there is nothing to export.
|
|
1009
|
+
paystack.disputes.export(from: "2025-01-01", to: "2025-04-30")
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
#### Answer a Dispute
|
|
1013
|
+
|
|
1014
|
+
```ruby
|
|
1015
|
+
# 1. Get a signed URL, upload the evidence file to it, and keep the returned file name
|
|
1016
|
+
upload = paystack.disputes.fetch_upload_url(id: 2867, upload_filename: "receipt.pdf")
|
|
1017
|
+
upload.data.signedUrl # upload your file to this URL
|
|
1018
|
+
upload.data.fileName # pass this as uploaded_filename
|
|
1019
|
+
|
|
1020
|
+
# 2. Add evidence
|
|
1021
|
+
paystack.disputes.add_evidence(
|
|
1022
|
+
id: 2867,
|
|
1023
|
+
customer_email: "ama@example.com",
|
|
1024
|
+
customer_name: "Ama Mensah",
|
|
1025
|
+
customer_phone: "0802345167",
|
|
1026
|
+
service_details: "Tithe payment for March",
|
|
1027
|
+
delivery_date: "2025-03-31"
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
# 3. Accept (with a refund) or decline. refund_amount is in the smallest currency unit.
|
|
1031
|
+
# It is `resolve` because `resolve` would shadow a method Ruby already has.
|
|
1032
|
+
paystack.disputes.resolve(
|
|
1033
|
+
id: 2867,
|
|
1034
|
+
resolution: "merchant-accepted", # or "declined"
|
|
1035
|
+
message: "Refunded at the customer's request",
|
|
1036
|
+
refund_amount: 50_000,
|
|
1037
|
+
uploaded_filename: upload.data.fileName
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
# Or change the refund amount and attachment without resolving
|
|
1041
|
+
paystack.disputes.update(id: 2867, refund_amount: 50_000, uploaded_filename: upload.data.fileName)
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
### Subaccounts
|
|
1045
|
+
|
|
1046
|
+
A subaccount is a settlement account that receives its share of the payments made to it, such as one per branch or partner. Pass its code (`ACCT_...`) as `subaccount` when you charge or initialize a transaction.
|
|
1047
|
+
|
|
1048
|
+
#### Create a Subaccount
|
|
1049
|
+
|
|
1050
|
+
```ruby
|
|
1051
|
+
# bank_code comes from paystack.banks.list (country: "ghana" lists GHS banks and the
|
|
1052
|
+
# MTN, VOD and ATL mobile money codes). account_number is a string: leading zeros matter.
|
|
1053
|
+
# percentage_charge is the percentage the main account keeps from each payment (0 to 100).
|
|
1054
|
+
response = paystack.subaccounts.create(
|
|
1055
|
+
business_name: "Grace Chapel Accra",
|
|
1056
|
+
bank_code: "MTN",
|
|
1057
|
+
account_number: "0551234987",
|
|
1058
|
+
percentage_charge: 2.5,
|
|
1059
|
+
description: "Giving for the Accra branch",
|
|
1060
|
+
primary_contact_name: "Ama Mensah",
|
|
1061
|
+
primary_contact_email: "finance@gracechapel.example",
|
|
1062
|
+
primary_contact_phone: "0551234987"
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
if response.success?
|
|
1066
|
+
puts "Created #{response.data.subaccount_code} for #{response.data.account_name}"
|
|
1067
|
+
else
|
|
1068
|
+
puts "Error: #{response.error_message}" # e.g. "Account details are invalid"
|
|
1069
|
+
end
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
Paystack's docs name the bank field `bank_code`; its OpenAPI spec calls it `settlement_bank`. The API reads both and prefers `bank_code`, so the SDK sends `bank_code`.
|
|
1073
|
+
|
|
1074
|
+
#### List and Fetch Subaccounts
|
|
1075
|
+
|
|
1076
|
+
```ruby
|
|
1077
|
+
# Paystack's page pagination (default 50 per page)
|
|
1078
|
+
paystack.subaccounts.list(per_page: 20, page: 2)
|
|
1079
|
+
|
|
1080
|
+
# Without active, the test API lists only active subaccounts. active: 0 lists the inactive
|
|
1081
|
+
# ones. Paystack reads any value other than 1, true included, as inactive, so pass 1 or 0.
|
|
1082
|
+
paystack.subaccounts.list(active: 0)
|
|
1083
|
+
|
|
1084
|
+
# Fetch by subaccount code or numeric ID
|
|
1085
|
+
paystack.subaccounts.fetch(id_or_code: "ACCT_6uujpqtzmnufzkw")
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
#### Update a Subaccount
|
|
1089
|
+
|
|
1090
|
+
```ruby
|
|
1091
|
+
# Send only what changes
|
|
1092
|
+
paystack.subaccounts.update(id_or_code: "ACCT_6uujpqtzmnufzkw", percentage_charge: 3)
|
|
1093
|
+
|
|
1094
|
+
# Deactivate (or reactivate with active: true)
|
|
1095
|
+
paystack.subaccounts.update(id_or_code: "ACCT_6uujpqtzmnufzkw", active: false)
|
|
1096
|
+
|
|
1097
|
+
# A new settlement account: Paystack needs bank_code and account_number together
|
|
1098
|
+
paystack.subaccounts.update(id_or_code: "ACCT_6uujpqtzmnufzkw", bank_code: "040100", account_number: "1234567890123")
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
### Balances
|
|
1102
|
+
|
|
1103
|
+
Read-only. Amounts are integers in the currency's subunit (pesewas for GHS, kobo for NGN), so `48400` is GHS 484.00. Paystack documents both operations on its Transfers Control page.
|
|
1104
|
+
|
|
1105
|
+
```ruby
|
|
1106
|
+
# One entry per currency your integration holds
|
|
1107
|
+
response = paystack.balances.fetch
|
|
1108
|
+
response.data.first.currency # => "GHS"
|
|
1109
|
+
response.data.first.balance # => 48400
|
|
1110
|
+
# Iterating (each, map) yields plain hashes with string keys
|
|
1111
|
+
response.data.each { |balance| puts "#{balance["currency"]}: #{balance["balance"]}" }
|
|
1112
|
+
|
|
1113
|
+
# Every pay-in and pay-out, newest first. `difference` is the signed change in the subunit
|
|
1114
|
+
# (-2500 for a refund), `balance` the running balance after it, and `model_responsible`
|
|
1115
|
+
# says what caused it ("Transaction", "Refund", "Transfer", ...).
|
|
1116
|
+
ledger = paystack.balances.ledger(per_page: 20, page: 1)
|
|
1117
|
+
ledger.data.each { |entry| puts "#{entry["createdAt"]} #{entry["model_responsible"]} #{entry["difference"]}" }
|
|
1118
|
+
ledger.meta # => total, skipped, perPage, page, pageCount (50 per page by default)
|
|
1119
|
+
|
|
1120
|
+
# Date filters work by calendar day (the time part is ignored). `to` includes that day;
|
|
1121
|
+
# `from` does not, so from: Date.new(2026, 10, 8) returns entries after 8 October.
|
|
1122
|
+
paystack.balances.ledger(from: Date.new(2026, 10, 1), to: Date.new(2026, 10, 31))
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
### Plans
|
|
1126
|
+
|
|
1127
|
+
A plan is a recurring charge definition: an amount, an interval and a currency. Customers are subscribed to a plan to be charged on that schedule. Paystack does not let you delete a plan.
|
|
1128
|
+
|
|
1129
|
+
#### Create a Plan
|
|
1130
|
+
|
|
1131
|
+
```ruby
|
|
1132
|
+
# amount is in the subunit of the currency (pesewas for GHS). Paystack's minimum is 2 GHS.
|
|
1133
|
+
# interval is one of hourly, daily, weekly, monthly, quarterly, biannually (every 6 months)
|
|
1134
|
+
# or annually, in lowercase. currency defaults to the integration's currency.
|
|
1135
|
+
response = paystack.plans.create(
|
|
1136
|
+
name: "Monthly tithe",
|
|
1137
|
+
amount: 20_000,
|
|
1138
|
+
interval: "monthly",
|
|
1139
|
+
currency: "GHS",
|
|
1140
|
+
description: "Standing tithe",
|
|
1141
|
+
send_invoices: false,
|
|
1142
|
+
send_sms: false,
|
|
1143
|
+
invoice_limit: 12
|
|
1144
|
+
)
|
|
1145
|
+
|
|
1146
|
+
if response.success?
|
|
1147
|
+
puts "Created #{response.data.plan_code}"
|
|
1148
|
+
else
|
|
1149
|
+
puts "Error: #{response.error_message}" # e.g. "Invalid interval selected"
|
|
1150
|
+
end
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
`send_invoices` and `send_sms` are booleans (Paystack's docs call `send_sms` a string; the API refuses anything that is not a boolean).
|
|
1154
|
+
|
|
1155
|
+
#### List and Fetch Plans
|
|
1156
|
+
|
|
1157
|
+
```ruby
|
|
1158
|
+
paystack.plans.list(per_page: 20, page: 1)
|
|
1159
|
+
|
|
1160
|
+
# Filter by interval, amount (in the subunit) or a date range
|
|
1161
|
+
paystack.plans.list(interval: "monthly", amount: 20_000)
|
|
1162
|
+
paystack.plans.list(from: "2026-01-01", to: "2026-12-31")
|
|
1163
|
+
|
|
1164
|
+
# Fetch by plan code or numeric ID
|
|
1165
|
+
paystack.plans.fetch(id_or_code: "PLN_gx2wn530m0i3w3m")
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
#### Update a Plan
|
|
1169
|
+
|
|
1170
|
+
```ruby
|
|
1171
|
+
# Send only what changes
|
|
1172
|
+
paystack.plans.update(id_or_code: "PLN_gx2wn530m0i3w3m", name: "Monthly tithe (renamed)")
|
|
1173
|
+
|
|
1174
|
+
# By default Paystack applies the change to the plan's existing subscriptions too. Pass
|
|
1175
|
+
# update_existing_subscriptions: false so that only new subscriptions use the new values.
|
|
1176
|
+
paystack.plans.update(id_or_code: "PLN_gx2wn530m0i3w3m", amount: 25_000, update_existing_subscriptions: false)
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
The response carries a message only, for example `"Plan updated. 1 subscription(s) affected"`.
|
|
1180
|
+
### Subscriptions
|
|
1181
|
+
|
|
1182
|
+
A subscription charges a customer's saved card on a plan's schedule. The customer needs a reusable card authorization (from an earlier successful card payment), and the plan is created in your Paystack dashboard or through the Plans API.
|
|
1183
|
+
|
|
1184
|
+
#### Create a Subscription
|
|
1185
|
+
|
|
1186
|
+
```ruby
|
|
1187
|
+
# customer: the customer's email or customer code. plan: the plan code (PLN_...).
|
|
1188
|
+
# authorization: which of the customer's saved cards to charge (AUTH_...); without it Paystack
|
|
1189
|
+
# uses the customer's most recent authorization. It must belong to this customer.
|
|
1190
|
+
# start_date: the date of the first debit, a Time, Date or ISO 8601 string. On the test API it
|
|
1191
|
+
# becomes next_payment_date (2027-01-15T10:00:00Z here), and the schedule follows the plan's interval.
|
|
1192
|
+
response = paystack.subscriptions.create(
|
|
1193
|
+
customer: "CUS_xnxdt6s1zg1f4nx",
|
|
1194
|
+
plan: "PLN_gx2wn530m0i3w3m",
|
|
1195
|
+
authorization: "AUTH_6tmt288t0o",
|
|
1196
|
+
start_date: Time.utc(2027, 1, 15, 10)
|
|
1197
|
+
)
|
|
1198
|
+
|
|
1199
|
+
if response.success?
|
|
1200
|
+
# Keep both: enable and disable need the code and the email token
|
|
1201
|
+
puts "#{response.data.subscription_code} #{response.data.email_token}"
|
|
1202
|
+
else
|
|
1203
|
+
puts "Error: #{response.error_message}" # e.g. "This subscription is already in place."
|
|
1204
|
+
end
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
A customer can hold one active subscription per plan. The SDK checks that `start_date` is ISO 8601 before sending it: on the test API, an invalid `start_date` is refused with a 400 but still leaves an active subscription with no payment date behind.
|
|
1208
|
+
|
|
1209
|
+
#### List and Fetch Subscriptions
|
|
1210
|
+
|
|
1211
|
+
```ruby
|
|
1212
|
+
# Filters take numeric IDs: the plan and customer codes match nothing
|
|
1213
|
+
paystack.subscriptions.list(plan_id: 4298026, customer_id: 406976740, per_page: 20, page: 1)
|
|
1214
|
+
|
|
1215
|
+
# Created within a window (Time, Date or ISO 8601 string)
|
|
1216
|
+
paystack.subscriptions.list(from: Date.new(2026, 10, 1), to: Time.now)
|
|
1217
|
+
|
|
1218
|
+
# Fetch by subscription code or numeric ID
|
|
1219
|
+
paystack.subscriptions.fetch(id_or_code: "SUB_vsyqdmlzble3uii")
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
#### Enable or Disable a Subscription
|
|
1223
|
+
|
|
1224
|
+
```ruby
|
|
1225
|
+
# token is the subscription's email_token, returned by create, list and fetch
|
|
1226
|
+
paystack.subscriptions.disable(code: "SUB_vsyqdmlzble3uii", token: "d7gofp6yppn3qz7")
|
|
1227
|
+
# => data.status "non-renewing"
|
|
1228
|
+
|
|
1229
|
+
paystack.subscriptions.enable(code: "SUB_vsyqdmlzble3uii", token: "d7gofp6yppn3qz7")
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
Disabling answers with `data.status` `non-renewing`, and fetch shows the same status. On the test API a disabled subscription that had never been charged could not be enabled again ("Subscription has been cancelled, and cannot be reactivated").
|
|
1233
|
+
|
|
1234
|
+
#### Let the Customer Update Their Card
|
|
1235
|
+
|
|
1236
|
+
```ruby
|
|
1237
|
+
# A link to a Paystack page where the customer changes the card on the subscription
|
|
1238
|
+
paystack.subscriptions.generate_update_link(code: "SUB_vsyqdmlzble3uii").data.link
|
|
1239
|
+
|
|
1240
|
+
# Or have Paystack email the customer that link
|
|
1241
|
+
paystack.subscriptions.send_update_link(code: "SUB_vsyqdmlzble3uii")
|
|
1242
|
+
```
|
|
1243
|
+
|
|
1244
|
+
Both take the subscription code; the numeric ID is not found.
|
|
1245
|
+
### Dedicated Virtual Accounts
|
|
1246
|
+
|
|
1247
|
+
A dedicated virtual account is a bank account number Paystack issues to one of your customers, so they can pay you by bank transfer. Paystack's docs offer it to Nigerian and Ghanaian businesses (`NGN` and `GHS`), and it has to be enabled for your business first: until it is, every call returns an unsuccessful response with status 403 and the message "Dedicated NUBAN is not available for your business".
|
|
1248
|
+
|
|
1249
|
+
#### Create or Assign an Account
|
|
1250
|
+
|
|
1251
|
+
```ruby
|
|
1252
|
+
# Which banks can issue an account for your integration (e.g. "wema-bank", "titan-paystack")
|
|
1253
|
+
paystack.dedicated_virtual_accounts.fetch_bank_providers
|
|
1254
|
+
|
|
1255
|
+
# For a customer you already created (their code or ID)
|
|
1256
|
+
response = paystack.dedicated_virtual_accounts.create(customer: "CUS_xnxdt6s1zg1f4nx", preferred_bank: "wema-bank")
|
|
1257
|
+
|
|
1258
|
+
if response.success?
|
|
1259
|
+
puts "#{response.data.account_number} at #{response.data.bank.name}"
|
|
1260
|
+
else
|
|
1261
|
+
puts "Error: #{response.error_message}"
|
|
1262
|
+
end
|
|
1263
|
+
|
|
1264
|
+
# Or create the customer, validate them and assign an account in one call. Paystack's docs
|
|
1265
|
+
# show the reply "Assign dedicated account in progress", with no account in it.
|
|
1266
|
+
paystack.dedicated_virtual_accounts.assign(
|
|
1267
|
+
email: "ama@example.com",
|
|
1268
|
+
first_name: "Ama",
|
|
1269
|
+
last_name: "Mensah",
|
|
1270
|
+
phone: "+2348100000000",
|
|
1271
|
+
preferred_bank: "wema-bank",
|
|
1272
|
+
country: "NG"
|
|
1273
|
+
)
|
|
1274
|
+
```
|
|
1275
|
+
|
|
1276
|
+
#### List, Fetch, Requery and Deactivate
|
|
1277
|
+
|
|
1278
|
+
```ruby
|
|
1279
|
+
paystack.dedicated_virtual_accounts.list(active: true, currency: "NGN", provider_slug: "wema-bank")
|
|
1280
|
+
|
|
1281
|
+
# By the account's numeric ID
|
|
1282
|
+
paystack.dedicated_virtual_accounts.fetch(dedicated_account_id: 1234553)
|
|
1283
|
+
|
|
1284
|
+
# Ask Paystack to check the account for transfers it has not recorded yet
|
|
1285
|
+
paystack.dedicated_virtual_accounts.requery(account_number: "1234567890", provider_slug: "wema-bank", date: Date.new(2026, 10, 9))
|
|
1286
|
+
|
|
1287
|
+
paystack.dedicated_virtual_accounts.deactivate(dedicated_account_id: 1234553)
|
|
1288
|
+
```
|
|
1289
|
+
|
|
1290
|
+
#### Split Payments on an Account
|
|
1291
|
+
|
|
1292
|
+
```ruby
|
|
1293
|
+
# Send payments into the account through a split (or a single subaccount:)
|
|
1294
|
+
paystack.dedicated_virtual_accounts.add_split(account_number: "0033322211", split_code: "SPL_e7jnRLtzla")
|
|
1295
|
+
|
|
1296
|
+
# And stop splitting them
|
|
1297
|
+
paystack.dedicated_virtual_accounts.remove_split(account_number: "0033322211")
|
|
1298
|
+
```
|
|
1299
|
+
|
|
1300
|
+
`create` and `assign` also take `split_code:` or `subaccount:` to set a split when the account is made.
|
|
1301
|
+
|
|
1302
|
+
### Payment Requests
|
|
1303
|
+
|
|
1304
|
+
A payment request is an invoice Paystack sends to a customer, who pays it online. Amounts are in the subunit (pesewas, kobo, cents).
|
|
1305
|
+
|
|
1306
|
+
#### Create a Payment Request
|
|
1307
|
+
|
|
1308
|
+
```ruby
|
|
1309
|
+
# customer is a customer code (CUS_...) or ID. Give line_items and tax, each an Array of Hashes,
|
|
1310
|
+
# and Paystack adds them up (2 x 2000 + 300 = 4300 here); give amount instead when you have none.
|
|
1311
|
+
response = paystack.payment_requests.create(
|
|
1312
|
+
customer: "CUS_p6i0reogc8ulu1n",
|
|
1313
|
+
currency: "GHS",
|
|
1314
|
+
description: "Harvest thanksgiving pledge",
|
|
1315
|
+
line_items: [{name: "Pledge", amount: 2000, quantity: 2}],
|
|
1316
|
+
tax: [{name: "Levy", amount: 300}],
|
|
1317
|
+
due_date: Date.new(2026, 12, 31), # a Date, a Time or an ISO 8601 string
|
|
1318
|
+
metadata: {branch: "Osu"}, # a Hash; Paystack refuses a JSON string here
|
|
1319
|
+
draft: true # a draft is not sent; finalize it later
|
|
1320
|
+
)
|
|
1321
|
+
|
|
1322
|
+
if response.success?
|
|
1323
|
+
puts "Created #{response.data.request_code} for #{response.data.amount}"
|
|
1324
|
+
else
|
|
1325
|
+
puts "Error: #{response.error_message}"
|
|
1326
|
+
end
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
Paystack's docs say it emails the customer when a request is created, unless you pass `draft: true` or `send_notification: false`. Without `amount`, `line_items` or `tax`, Paystack refuses anything but a draft ("Amount was not passed or could not be extrapolated from line items.").
|
|
1330
|
+
|
|
1331
|
+
#### List, Fetch and Verify Payment Requests
|
|
1332
|
+
|
|
1333
|
+
```ruby
|
|
1334
|
+
# customer_id is the numeric customer ID (Paystack ignores a CUS_ code here)
|
|
1335
|
+
paystack.payment_requests.list(customer_id: 407172149, status: "pending", per_page: 20)
|
|
1336
|
+
|
|
1337
|
+
# Archived requests are left out unless you pass include_archive: true. Paystack includes them
|
|
1338
|
+
# whenever the parameter is sent, even as false, so leave it out rather than passing false.
|
|
1339
|
+
paystack.payment_requests.list(include_archive: true)
|
|
1340
|
+
|
|
1341
|
+
# Fetch by numeric ID or PRQ_ code
|
|
1342
|
+
paystack.payment_requests.fetch(id_or_code: "PRQ_7w7dpecncnebg7e")
|
|
1343
|
+
|
|
1344
|
+
# Verify takes the PRQ_ code only, and only once the request is no longer a draft
|
|
1345
|
+
paystack.payment_requests.verify(code: "PRQ_7w7dpecncnebg7e")
|
|
1346
|
+
|
|
1347
|
+
# Pending, successful and total amounts per currency
|
|
1348
|
+
paystack.payment_requests.totals
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
#### Update, Finalize, Notify and Archive
|
|
1352
|
+
|
|
1353
|
+
```ruby
|
|
1354
|
+
# Send only what changes; new line_items and tax recompute the amount
|
|
1355
|
+
paystack.payment_requests.update(id_or_code: "PRQ_7w7dpecncnebg7e", due_date: "2027-01-31")
|
|
1356
|
+
|
|
1357
|
+
# Finalize a draft. Paystack emails the customer unless send_notification is false.
|
|
1358
|
+
paystack.payment_requests.finalize(id_or_code: "PRQ_7w7dpecncnebg7e", send_notification: false)
|
|
1359
|
+
|
|
1360
|
+
# Email the customer a reminder
|
|
1361
|
+
paystack.payment_requests.notify(code: "PRQ_7w7dpecncnebg7e")
|
|
1362
|
+
|
|
1363
|
+
# Archive: it no longer shows up in list or verify
|
|
1364
|
+
paystack.payment_requests.archive(id_or_code: "PRQ_7w7dpecncnebg7e")
|
|
1365
|
+
```
|
|
897
1366
|
|
|
898
1367
|
### Response Handling
|
|
899
1368
|
|
data/lib/paystack_sdk/client.rb
CHANGED
|
@@ -8,6 +8,15 @@ require_relative "resources/banks"
|
|
|
8
8
|
require_relative "resources/charges"
|
|
9
9
|
require_relative "resources/miscellaneous"
|
|
10
10
|
require_relative "resources/refunds"
|
|
11
|
+
require_relative "resources/settlements"
|
|
12
|
+
require_relative "resources/splits"
|
|
13
|
+
require_relative "resources/disputes"
|
|
14
|
+
require_relative "resources/subaccounts"
|
|
15
|
+
require_relative "resources/balances"
|
|
16
|
+
require_relative "resources/plans"
|
|
17
|
+
require_relative "resources/subscriptions"
|
|
18
|
+
require_relative "resources/dedicated_virtual_accounts"
|
|
19
|
+
require_relative "resources/payment_requests"
|
|
11
20
|
require_relative "utils/connection_utils"
|
|
12
21
|
|
|
13
22
|
module PaystackSdk
|
|
@@ -173,6 +182,78 @@ module PaystackSdk
|
|
|
173
182
|
@refunds ||= Resources::Refunds.new(@connection)
|
|
174
183
|
end
|
|
175
184
|
|
|
185
|
+
# Provides access to the `Settlements` resource.
|
|
186
|
+
#
|
|
187
|
+
# @return [PaystackSdk::Resources::Settlements] An instance of the
|
|
188
|
+
# `Settlements` resource.
|
|
189
|
+
def settlements
|
|
190
|
+
@settlements ||= Resources::Settlements.new(@connection)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Provides access to the `Splits` resource.
|
|
194
|
+
#
|
|
195
|
+
# @return [PaystackSdk::Resources::Splits] An instance of the
|
|
196
|
+
# `Splits` resource.
|
|
197
|
+
def splits
|
|
198
|
+
@splits ||= Resources::Splits.new(@connection)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Provides access to the `Disputes` resource.
|
|
202
|
+
#
|
|
203
|
+
# @return [PaystackSdk::Resources::Disputes] An instance of the
|
|
204
|
+
# `Disputes` resource.
|
|
205
|
+
def disputes
|
|
206
|
+
@disputes ||= Resources::Disputes.new(@connection)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Provides access to the `Subaccounts` resource.
|
|
210
|
+
#
|
|
211
|
+
# @return [PaystackSdk::Resources::Subaccounts] An instance of the
|
|
212
|
+
# `Subaccounts` resource.
|
|
213
|
+
def subaccounts
|
|
214
|
+
@subaccounts ||= Resources::Subaccounts.new(@connection)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Provides access to the `Balances` resource.
|
|
218
|
+
#
|
|
219
|
+
# @return [PaystackSdk::Resources::Balances] An instance of the
|
|
220
|
+
# `Balances` resource.
|
|
221
|
+
def balances
|
|
222
|
+
@balances ||= Resources::Balances.new(@connection)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Provides access to the `Plans` resource.
|
|
226
|
+
#
|
|
227
|
+
# @return [PaystackSdk::Resources::Plans] An instance of the
|
|
228
|
+
# `Plans` resource.
|
|
229
|
+
def plans
|
|
230
|
+
@plans ||= Resources::Plans.new(@connection)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Provides access to the `Subscriptions` resource.
|
|
234
|
+
#
|
|
235
|
+
# @return [PaystackSdk::Resources::Subscriptions] An instance of the
|
|
236
|
+
# `Subscriptions` resource.
|
|
237
|
+
def subscriptions
|
|
238
|
+
@subscriptions ||= Resources::Subscriptions.new(@connection)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Provides access to the `DedicatedVirtualAccounts` resource.
|
|
242
|
+
#
|
|
243
|
+
# @return [PaystackSdk::Resources::DedicatedVirtualAccounts] An instance of the
|
|
244
|
+
# `DedicatedVirtualAccounts` resource.
|
|
245
|
+
def dedicated_virtual_accounts
|
|
246
|
+
@dedicated_virtual_accounts ||= Resources::DedicatedVirtualAccounts.new(@connection)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Provides access to the `PaymentRequests` resource.
|
|
250
|
+
#
|
|
251
|
+
# @return [PaystackSdk::Resources::PaymentRequests] An instance of the
|
|
252
|
+
# `PaymentRequests` resource.
|
|
253
|
+
def payment_requests
|
|
254
|
+
@payment_requests ||= Resources::PaymentRequests.new(@connection)
|
|
255
|
+
end
|
|
256
|
+
|
|
176
257
|
private
|
|
177
258
|
|
|
178
259
|
# The secret key a connection sends, read from its Authorization header.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Generated by bin/paystack-scaffold from Paystack's OpenAPI spec (PaystackOSS/openapi@d9d444d).
|
|
4
|
+
# Do not edit by hand: regenerate it with `bin/paystack-scaffold Balance`. Hand-written extras go in lib/paystack_sdk/resources/extensions/balances.rb.
|
|
5
|
+
# scaffold-digest: 65e146fbcd39c274107a8234988e1b3b51e3c58a5e74d7c4b0bf9c4f084a9b83
|
|
6
|
+
|
|
7
|
+
require_relative "base"
|
|
8
|
+
|
|
9
|
+
module PaystackSdk
|
|
10
|
+
module Resources
|
|
11
|
+
# Balance operations.
|
|
12
|
+
class Balances < PaystackSdk::Resources::Base
|
|
13
|
+
# Ruby keyword => the parameter name Paystack documents.
|
|
14
|
+
WIRE_NAMES = {per_page: "perPage"}.freeze
|
|
15
|
+
|
|
16
|
+
# Fetch Balance.
|
|
17
|
+
#
|
|
18
|
+
# Fetch the available balance on your integration
|
|
19
|
+
#
|
|
20
|
+
# @return [PaystackSdk::Response] The response from the Paystack API.
|
|
21
|
+
# @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
|
|
22
|
+
# @see https://paystack.com/docs/api/transfer-control/#balance
|
|
23
|
+
def fetch
|
|
24
|
+
handle_response(@connection.get("/balance"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Balance Ledger.
|
|
28
|
+
#
|
|
29
|
+
# Fetch all pay-ins and pay-outs that occured on your integration
|
|
30
|
+
#
|
|
31
|
+
# @param per_page [Integer] Number of records to fetch per page
|
|
32
|
+
# @param page [Integer] The section to retrieve
|
|
33
|
+
# @param from [String] The start date
|
|
34
|
+
# @param to [String] The end date
|
|
35
|
+
# @return [PaystackSdk::Response] The response from the Paystack API.
|
|
36
|
+
# @raise [PaystackSdk::Error] If a parameter is invalid or the API request fails.
|
|
37
|
+
# @see https://paystack.com/docs/api/transfer-control/#balance-ledger
|
|
38
|
+
def ledger(per_page: nil, page: nil, from: nil, to: nil)
|
|
39
|
+
validate_positive_integer!(value: per_page, name: "perPage")
|
|
40
|
+
validate_positive_integer!(value: page, name: "page")
|
|
41
|
+
|
|
42
|
+
wire_query = to_wire(
|
|
43
|
+
{
|
|
44
|
+
per_page:,
|
|
45
|
+
page:,
|
|
46
|
+
from: format_datetime(from, name: "from"),
|
|
47
|
+
to: format_datetime(to, name: "to")
|
|
48
|
+
},
|
|
49
|
+
WIRE_NAMES
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
handle_response(@connection.get("/balance/ledger", wire_query))
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|