paddle 2.9 → 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 +60 -0
- data/LICENSE.txt +21 -0
- data/README.md +301 -73
- data/lib/paddle/classic/client.rb +2 -2
- data/lib/paddle/client.rb +31 -15
- data/lib/paddle/collection.rb +47 -11
- data/lib/paddle/configuration.rb +68 -3
- data/lib/paddle/error_generator.rb +4 -5
- data/lib/paddle/models/adjustment.rb +6 -6
- data/lib/paddle/models/customer.rb +8 -2
- data/lib/paddle/models/explore_entity.rb +4 -0
- data/lib/paddle/models/explore_result.rb +50 -0
- data/lib/paddle/models/metric.rb +55 -0
- data/lib/paddle/models/notification.rb +6 -5
- data/lib/paddle/models/report.rb +1 -3
- data/lib/paddle/models/simulation.rb +2 -2
- data/lib/paddle/models/simulation_run.rb +2 -2
- data/lib/paddle/models/subscription.rb +12 -0
- data/lib/paddle/models/subscription_history.rb +4 -0
- data/lib/paddle/models/transaction.rb +9 -4
- data/lib/paddle/object.rb +91 -16
- data/lib/paddle/version.rb +1 -1
- data/lib/paddle/webhook.rb +65 -0
- data/lib/paddle.rb +27 -1
- metadata +25 -47
- data/.env.example +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -110
- data/Rakefile +0 -12
- data/bin/console +0 -25
- data/bin/setup +0 -8
- data/mise.toml +0 -2
- data/paddle.gemspec +0 -32
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae96654d26047a02fed95576d33befa70e04ae3103e4f191de9f8a8a6e71bdb8
|
|
4
|
+
data.tar.gz: 3e434eabe578af9f29e9fd543b829a691f8afc8b2c6f4cb2fbf1f9334a75ea97
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: abc283ed4ca2bda5cc36ff72c576fafeba7b858ed4a87b807ead8187c276690b8f995e8f6be51be41f8ac7dab90d897e43a7aca5baea38eb227465f956ef1aa3
|
|
7
|
+
data.tar.gz: 53f563a9dec92f02717c6cf46a953fd31f605b8888ca04e6e1b61c488a6372ec94f1703aad21e43a9198da7c265689e4046b29613e76a5728de9d0fa2bf2b5a5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 3.0 - 2026-09-27
|
|
4
|
+
|
|
5
|
+
### Breaking changes
|
|
6
|
+
|
|
7
|
+
- **Ruby 3.3 or later is required.** Ruby 3.2 has reached end of life.
|
|
8
|
+
- **Responses are no longer OpenStructs.** `Paddle::Object` is now a lightweight class, and nested objects are
|
|
9
|
+
`Paddle::Object` rather than `OpenStruct`. Dot access, hash access (`obj[:id]`, `obj["id"]`), setters, `nil` for
|
|
10
|
+
missing attributes, `each_pair` and `update` work as before. Code that checks `is_a?(OpenStruct)` or uses
|
|
11
|
+
OpenStruct-only methods, such as `delete_field` or `to_h` with a block, will need updating. Attributes named after
|
|
12
|
+
built-in methods, such as `hash` or `class`, need to be read with `obj[:hash]`.
|
|
13
|
+
- **`to_h` now converts nested objects to hashes too**, rather than only the top level.
|
|
14
|
+
- **The `ostruct` and `cgi` dependencies have been removed.** If your app uses either and relied on this gem to install
|
|
15
|
+
it, add it to your Gemfile.
|
|
16
|
+
- **Every non-2xx response now raises an error.** Previously only a fixed list of status codes raised, so responses
|
|
17
|
+
like 422, 502 and 504 were returned as if they'd succeeded. Non-JSON error bodies, such as an HTML page from a
|
|
18
|
+
gateway, also raise now rather than causing a `NoMethodError`.
|
|
19
|
+
- **The environment is detected from the API key.** Keys starting with `pdl_live_` or `pdl_sdbx_` set the environment
|
|
20
|
+
automatically, so a sandbox key with no environment set now goes to the sandbox rather than production. Setting an
|
|
21
|
+
environment that doesn't match the key raises an `ArgumentError`. Older keys without a prefix behave as before.
|
|
22
|
+
- **Config changes take effect straight away.** The connection used to keep the API key, environment, version and
|
|
23
|
+
connection options from the first request, so later changes were ignored.
|
|
24
|
+
- **Array query params are sent as comma-separated lists**, e.g. `status=active,past_due`, as Paddle expects. They
|
|
25
|
+
were sent as `status[]=active&status[]=past_due`, which Paddle didn't read as intended.
|
|
26
|
+
- `ServiceUnavailableError` (503) now says the API is temporarily unavailable, rather than describing a rate limit.
|
|
27
|
+
Rate limits are raised as `TooManyRequestsError` (429).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `Paddle.with_config` to make requests with a different API key, environment or version, such as for another
|
|
32
|
+
Paddle account. It's safe to use in multi-threaded servers and job runners.
|
|
33
|
+
- `Paddle::Webhook` for verifying webhook signatures, with `verify!`, `valid?` and `construct_event`.
|
|
34
|
+
- `Paddle::Metric` for the metrics API: `monthly_recurring_revenue`, `monthly_recurring_revenue_change`,
|
|
35
|
+
`active_subscribers`, `revenue`, `refunds`, `chargebacks` and `checkout_conversion`.
|
|
36
|
+
- The Explore metrics API, with `Metric.explore_entities` and `Metric.explore`.
|
|
37
|
+
- `Collection#has_more?`, `next_page` and `auto_paging_each` for paging through results.
|
|
38
|
+
- `skip_count: true` on any list method, to skip counting results for faster responses.
|
|
39
|
+
- `Subscription.history`, `Subscription.charge_preview` and `Transaction.revise`.
|
|
40
|
+
- `Customer.credit_balances` to return the credit balance for every currency.
|
|
41
|
+
- `Notification.replay` has been restored.
|
|
42
|
+
- `Adjustment.create` no longer needs `items:` when `type: "full"` is given.
|
|
43
|
+
- `Simulation.runs` and `SimulationRun.events` accept pagination params.
|
|
44
|
+
- `Paddle::Object#to_json`, `as_json`, `dig` and `key?`.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- Building response objects is around 20x faster and uses around 20x less memory.
|
|
49
|
+
- Faraday 2.14.3 or later is required, for [GHSA-98m9-hrrm-r99r](https://github.com/advisories/GHSA-98m9-hrrm-r99r).
|
|
50
|
+
- The repository has moved to [d34ndev/paddle](https://github.com/d34ndev/paddle).
|
|
51
|
+
- The Classic API docs have moved to [docs/classic.md](docs/classic.md), and now cover every Classic endpoint.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
- `delete_request` passed headers to Faraday as query params.
|
|
56
|
+
- Calling `update` on an object whose model doesn't support updating now raises a `NoMethodError`.
|
|
57
|
+
|
|
58
|
+
## Earlier versions
|
|
59
|
+
|
|
60
|
+
See the [releases on GitHub](https://github.com/d34ndev/paddle/releases) for versions before 3.0.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2021-2026 Dean Perry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
The easiest and most complete Ruby library for the Paddle APIs, both Classic and Billing.
|
|
4
4
|
|
|
5
|
+
Using Paddle Classic? See the [Classic API docs](docs/classic.md).
|
|
6
|
+
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
7
9
|
Add this line to your application's Gemfile:
|
|
8
10
|
|
|
9
11
|
```ruby
|
|
10
|
-
gem "paddle", "~>
|
|
12
|
+
gem "paddle", "~> 3.0"
|
|
11
13
|
```
|
|
12
14
|
|
|
13
15
|
## Billing API
|
|
@@ -33,12 +35,75 @@ Paddle.configure do |config|
|
|
|
33
35
|
end
|
|
34
36
|
```
|
|
35
37
|
|
|
38
|
+
API keys created since May 2025 start with `pdl_live_` or `pdl_sdbx_`, so the environment is detected from the key and you don't need to set it:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Paddle.configure do |config|
|
|
42
|
+
config.api_key = "pdl_sdbx_apikey_..."
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Paddle.config.environment
|
|
46
|
+
#=> :sandbox
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
If you do set an environment that doesn't match the key, such as `:production` with a `pdl_sdbx_` key,
|
|
50
|
+
an `ArgumentError` is raised straight away, since those requests would always fail. Older API keys don't have a prefix, so set the environment for them as before. It defaults to `:production`.
|
|
51
|
+
|
|
52
|
+
### Using Multiple API Keys
|
|
53
|
+
|
|
54
|
+
To make requests with a different API key, environment or version, such as for another Paddle account,
|
|
55
|
+
wrap them in `Paddle.with_config`. Everything in the block uses the given options over the global config:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
Paddle.with_config(api_key: account.paddle_api_key) do
|
|
59
|
+
Paddle::Subscription.list(status: "active")
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Blocks can be nested, and you can change the environment or version too
|
|
63
|
+
Paddle.with_config(api_key: "pdl_live_apikey_...", version: 1) do
|
|
64
|
+
Paddle::Product.list
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
When a new API key is given without an environment, the environment is detected from the key. For older keys without a prefix, the current environment is kept.
|
|
69
|
+
|
|
70
|
+
The config is stored per thread and fiber, so it's safe to use in multi-threaded servers like Puma and job
|
|
71
|
+
runners like Sidekiq. Concurrent requests never see each other's keys. Threads and fibers started inside the block use the same config. `Paddle.configure` always changes the global config, even inside a block.
|
|
72
|
+
|
|
73
|
+
### Connection Options
|
|
74
|
+
|
|
75
|
+
You can pass [options](https://lostisland.github.io/faraday/#/customization/connection-options) to the underlying [Faraday](https://lostisland.github.io/faraday/) connection using `connection_options`. This is useful for setting timeouts, proxies, or SSL configuration:
|
|
76
|
+
|
|
77
|
+
```ruby
|
|
78
|
+
Paddle.configure do |config|
|
|
79
|
+
config.environment = :sandbox
|
|
80
|
+
config.api_key = ENV["PADDLE_API_KEY"]
|
|
81
|
+
|
|
82
|
+
config.connection_options = {
|
|
83
|
+
request: { timeout: 10, open_timeout: 5 }
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
36
88
|
### Resources
|
|
37
89
|
|
|
38
90
|
The gem maps as closely as we can to the Paddle API so you can easily convert API examples to gem code.
|
|
39
91
|
|
|
40
|
-
Responses are created as objects like `Paddle::Product`. Having types like `Paddle::Product` is handy for understanding what
|
|
41
|
-
|
|
92
|
+
Responses are created as objects like `Paddle::Product`. Having types like `Paddle::Product` is handy for understanding what type of object you're working with. Attributes can be read with dot notation or like a hash, and nested data is wrapped too:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
transaction = Paddle::Transaction.retrieve(id: "txn_abc123")
|
|
96
|
+
|
|
97
|
+
transaction.status #=> "completed"
|
|
98
|
+
transaction[:status] #=> "completed"
|
|
99
|
+
transaction.details.totals.total #=> "1000"
|
|
100
|
+
transaction.dig(:items, 0, :price, :id) #=> "pri_abc123"
|
|
101
|
+
transaction.missing_attribute #=> nil
|
|
102
|
+
|
|
103
|
+
# Convert back to a hash or JSON, including nested data
|
|
104
|
+
transaction.to_h
|
|
105
|
+
transaction.to_json
|
|
106
|
+
```
|
|
42
107
|
|
|
43
108
|
### Pagination
|
|
44
109
|
|
|
@@ -51,8 +116,14 @@ results = Paddle::Product.list(per_page: 10)
|
|
|
51
116
|
#=> Paddle::Collection
|
|
52
117
|
|
|
53
118
|
results.total
|
|
119
|
+
#=> 42
|
|
120
|
+
|
|
121
|
+
results.per_page
|
|
54
122
|
#=> 10
|
|
55
123
|
|
|
124
|
+
results.has_more?
|
|
125
|
+
#=> true
|
|
126
|
+
|
|
56
127
|
results.data
|
|
57
128
|
#=> [#<Paddle::Product>, #<Paddle::Product>]
|
|
58
129
|
|
|
@@ -66,9 +137,37 @@ results.first
|
|
|
66
137
|
results.last
|
|
67
138
|
#=> #<Paddle::Product>
|
|
68
139
|
|
|
69
|
-
# Retrieve the next page
|
|
140
|
+
# Retrieve the next page. Returns nil when there are no more pages
|
|
141
|
+
results.next_page
|
|
142
|
+
#=> Paddle::Collection
|
|
143
|
+
|
|
144
|
+
# Or use the after cursor directly
|
|
70
145
|
Paddle::Product.list(per_page: 10, after: "abc123")
|
|
71
146
|
#=> Paddle::Collection
|
|
147
|
+
|
|
148
|
+
# Iterate over every result across all pages, fetching each page as it's needed
|
|
149
|
+
Paddle::Product.list(per_page: 50).auto_paging_each do |product|
|
|
150
|
+
puts product.id
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Without a block, auto_paging_each returns an Enumerator. Pages are only fetched until a match is found
|
|
154
|
+
Paddle::Customer.list.auto_paging_each.find { |customer| customer.email == "michael@mycompany.com" }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
>[!NOTE]
|
|
158
|
+
>
|
|
159
|
+
> `total` is Paddle's `estimated_total`. For lists of more than 100,000 results it's capped at `100001`,
|
|
160
|
+
> and it's `-1` when Paddle can't count the results. Use `has_more?`, `next_page` or `auto_paging_each`
|
|
161
|
+
> to page through results rather than relying on `total`.
|
|
162
|
+
|
|
163
|
+
If you don't need `total`, pass `skip_count: true` to any list method. This sends the `Skip-Count` header,
|
|
164
|
+
so Paddle skips counting the results and responds faster. `total` will be `-1`, and `next_page` and
|
|
165
|
+
`auto_paging_each` keep sending the header for later pages.
|
|
166
|
+
|
|
167
|
+
```ruby
|
|
168
|
+
Paddle::Transaction.list(skip_count: true).auto_paging_each do |transaction|
|
|
169
|
+
puts transaction.id
|
|
170
|
+
end
|
|
72
171
|
```
|
|
73
172
|
|
|
74
173
|
### Caveats
|
|
@@ -77,6 +176,14 @@ Paddle::Product.list(per_page: 10, after: "abc123")
|
|
|
77
176
|
>
|
|
78
177
|
> The Paddle API doesn't take `nil` values for optional parameters. If you want to remove a value, you'll need to pass `"null"` instead.
|
|
79
178
|
|
|
179
|
+
When filtering a list, you can pass an array or a comma-separated string to filter by more than one value. Arrays are sent as comma-separated lists, as Paddle expects:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
Paddle::Subscription.list(status: [ "active", "past_due" ])
|
|
183
|
+
# is the same as
|
|
184
|
+
Paddle::Subscription.list(status: "active,past_due")
|
|
185
|
+
```
|
|
186
|
+
|
|
80
187
|
### Error Handling
|
|
81
188
|
|
|
82
189
|
When API requests fail, the gem provides detailed error information to help you debug issues. Errors are raised as exceptions with comprehensive details including field-level validation errors.
|
|
@@ -84,6 +191,7 @@ When API requests fail, the gem provides detailed error information to help you
|
|
|
84
191
|
#### Error Structure
|
|
85
192
|
|
|
86
193
|
All errors inherit from `Paddle::ErrorGenerator` and include:
|
|
194
|
+
|
|
87
195
|
- HTTP status code
|
|
88
196
|
- Error code from Paddle
|
|
89
197
|
- Detailed error message
|
|
@@ -100,6 +208,7 @@ All errors inherit from `Paddle::ErrorGenerator` and include:
|
|
|
100
208
|
- `Paddle::Errors::ConflictError` (409) - Request conflicts with existing data
|
|
101
209
|
- `Paddle::Errors::TooManyRequestsError` (429) - Rate limit exceeded
|
|
102
210
|
- `Paddle::Errors::InternalError` (500) - Server error
|
|
211
|
+
- `Paddle::Errors::NotImplementedError` (501) - Resource not implemented
|
|
103
212
|
- `Paddle::Errors::ServiceUnavailableError` (503) - Service unavailable
|
|
104
213
|
|
|
105
214
|
#### Error Example
|
|
@@ -275,8 +384,12 @@ customer.update(status: "archived")
|
|
|
275
384
|
# or
|
|
276
385
|
Paddle::Customer.update(id: "ctm_abc123", status: "archived")
|
|
277
386
|
|
|
278
|
-
#
|
|
387
|
+
# List credit balances for a customer. Customers have a balance for each currency
|
|
279
388
|
# https://developer.paddle.com/api-reference/customers/list-credit-balances
|
|
389
|
+
Paddle::Customer.credit_balances(id: "ctm_abc123")
|
|
390
|
+
Paddle::Customer.credit_balances(id: "ctm_abc123", currency_code: "USD")
|
|
391
|
+
|
|
392
|
+
# Retrieve the first credit balance for a customer
|
|
280
393
|
Paddle::Customer.credit(id: "ctm_abc123")
|
|
281
394
|
|
|
282
395
|
# Generate an authentication token for a customer
|
|
@@ -348,21 +461,32 @@ Paddle::Transaction.retrieve(id: "txn_abc123")
|
|
|
348
461
|
transaction = Paddle::Transaction.retrieve(id: "txn_abc123", extra: "customer")
|
|
349
462
|
|
|
350
463
|
# Update a transaction
|
|
351
|
-
# https://developer.paddle.com/api-reference/
|
|
464
|
+
# https://developer.paddle.com/api-reference/transactions/update-transaction
|
|
352
465
|
transaction.update(items: [ { price_id: "pri_abc123", quantity: 2 } ])
|
|
353
466
|
# or
|
|
354
467
|
Paddle::Transaction.update(id: "txn_abc123", items: [ { price_id: "pri_abc123", quantity: 2 } ])
|
|
355
468
|
|
|
356
469
|
# Preview a transaction
|
|
357
|
-
# https://developer.paddle.com/api-reference/
|
|
470
|
+
# https://developer.paddle.com/api-reference/transactions/preview-transaction
|
|
358
471
|
Paddle::Transaction.preview(items: [ { price_id: "pri_123abc", quantity: 5 } ])
|
|
359
472
|
|
|
360
473
|
# Get a PDF invoice for a transaction
|
|
361
474
|
# disposition defaults to "attachment"
|
|
362
475
|
# Returns a raw URL. This URL is not permanent and will expire.
|
|
363
|
-
# https://developer.paddle.com/api-reference/
|
|
476
|
+
# https://developer.paddle.com/api-reference/transactions/get-invoice-pdf
|
|
364
477
|
Paddle::Transaction.invoice(id: "txn_abc123", disposition: "inline")
|
|
365
478
|
#=> https://paddle-sandbox-invoice...
|
|
479
|
+
|
|
480
|
+
# Revise customer, business and address details on a billed or completed transaction
|
|
481
|
+
# Only address lines, city and region can be changed, and a transaction can only be revised once.
|
|
482
|
+
# The related customer, business and address records aren't updated
|
|
483
|
+
# https://developer.paddle.com/api-reference/transactions/revise-transaction
|
|
484
|
+
Paddle::Transaction.revise(
|
|
485
|
+
id: "txn_abc123",
|
|
486
|
+
customer: { name: "Sam Miller" },
|
|
487
|
+
business: { tax_identifier: "AB0123456789" },
|
|
488
|
+
address: { first_line: "3811 Ditmars Blvd" }
|
|
489
|
+
)
|
|
366
490
|
```
|
|
367
491
|
|
|
368
492
|
### Subscriptions
|
|
@@ -402,6 +526,11 @@ Paddle::Subscription.get_transaction(id: "sub_abc123")
|
|
|
402
526
|
# https://developer.paddle.com/api-reference/subscriptions/create-one-time-charge
|
|
403
527
|
Paddle::Subscription.charge(id: "sub_abc123", items: [ { price_id: "pri_123abc", quantity: 2 } ], effective_from: "immediately")
|
|
404
528
|
|
|
529
|
+
# Preview a one-time charge for a subscription without billing it
|
|
530
|
+
# Returns a Subscription with immediate_transaction and next_transaction previews
|
|
531
|
+
# https://developer.paddle.com/api-reference/subscriptions/preview-subscription-charge
|
|
532
|
+
Paddle::Subscription.charge_preview(id: "sub_abc123", items: [ { price_id: "pri_123abc", quantity: 2 } ], effective_from: "immediately")
|
|
533
|
+
|
|
405
534
|
# Pause a subscription
|
|
406
535
|
# https://developer.paddle.com/api-reference/subscriptions/pause-subscription
|
|
407
536
|
Paddle::Subscription.pause(id: "sub_abc123")
|
|
@@ -421,6 +550,18 @@ Paddle::Subscription.cancel(id: "sub_abc123", effective_from: "immediately")
|
|
|
421
550
|
# Activate a trialing subscription
|
|
422
551
|
# https://developer.paddle.com/api-reference/subscriptions/activate-subscription
|
|
423
552
|
Paddle::Subscription.activate(id: "sub_abc123")
|
|
553
|
+
|
|
554
|
+
# List the history of a subscription, newest first
|
|
555
|
+
# Returns a Paddle::Collection of Paddle::SubscriptionHistory
|
|
556
|
+
# https://developer.paddle.com/api-reference/subscription-history/list-subscription-history
|
|
557
|
+
Paddle::Subscription.history(id: "sub_abc123")
|
|
558
|
+
Paddle::Subscription.history(id: "sub_abc123", action: [ "subscription_created", "subscription_canceled" ])
|
|
559
|
+
Paddle::Subscription.history(id: "sub_abc123", source: "customer_portal", actor_type: "customer")
|
|
560
|
+
Paddle::Subscription.history(id: "sub_abc123", "occurred_at[GTE]": "2026-01-01T00:00:00Z")
|
|
561
|
+
|
|
562
|
+
Paddle::Subscription.history(id: "sub_abc123").auto_paging_each do |entry|
|
|
563
|
+
puts "#{entry.occurred_at} #{entry.detail.action} by #{entry.actor.type} via #{entry.source}"
|
|
564
|
+
end
|
|
424
565
|
```
|
|
425
566
|
|
|
426
567
|
### Customer Portal Sessions
|
|
@@ -432,7 +573,6 @@ Paddle::PortalSession.create customer: "ctm_abc123"
|
|
|
432
573
|
Paddle::PortalSession.create customer: "ctm_abc123", subscription_ids: ["sub_abc123"]
|
|
433
574
|
```
|
|
434
575
|
|
|
435
|
-
|
|
436
576
|
### Adjustments
|
|
437
577
|
|
|
438
578
|
```ruby
|
|
@@ -451,11 +591,38 @@ Paddle::Adjustment.create(
|
|
|
451
591
|
items: [
|
|
452
592
|
{
|
|
453
593
|
type: "full",
|
|
454
|
-
item_id: "
|
|
594
|
+
item_id: "txnitm_abc123"
|
|
595
|
+
}
|
|
596
|
+
]
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# Partially refund an item
|
|
600
|
+
# amount is in the lowest denomination of the currency, e.g. "500" is $5.00 for USD
|
|
601
|
+
Paddle::Adjustment.create(
|
|
602
|
+
action: "refund",
|
|
603
|
+
transaction_id: "txn_abc123",
|
|
604
|
+
reason: "Requested by customer",
|
|
605
|
+
items: [
|
|
606
|
+
{
|
|
607
|
+
type: "partial",
|
|
608
|
+
item_id: "txnitm_abc123",
|
|
609
|
+
amount: "500"
|
|
455
610
|
}
|
|
456
611
|
]
|
|
457
612
|
)
|
|
458
613
|
|
|
614
|
+
# Refund or credit the grand total of a transaction without specifying items
|
|
615
|
+
Paddle::Adjustment.create(
|
|
616
|
+
action: "refund",
|
|
617
|
+
transaction_id: "txn_abc123",
|
|
618
|
+
reason: "Requested by customer",
|
|
619
|
+
type: "full"
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
# Refunds are created with a status of "pending_approval" and are reviewed by Paddle
|
|
623
|
+
# before being processed. Listen for the adjustment.updated webhook to know when
|
|
624
|
+
# a refund has been approved or rejected.
|
|
625
|
+
|
|
459
626
|
# Get a credit note for an adjustment
|
|
460
627
|
# disposition defaults to "attachment"
|
|
461
628
|
# Returns a raw URL. This URL is not permanent and will expire.
|
|
@@ -546,9 +713,11 @@ Paddle::Notification.list(status: "failed")
|
|
|
546
713
|
Paddle::Notification.retrieve(id: "ntf_abc123")
|
|
547
714
|
|
|
548
715
|
# Replay a notification
|
|
549
|
-
#
|
|
550
|
-
#
|
|
716
|
+
# Creates a new notification for the same event. Only delivered or failed notifications
|
|
717
|
+
# with an origin of "event" can be replayed. Returns the new notification_id
|
|
718
|
+
# https://developer.paddle.com/api-reference/notifications/replay-notification
|
|
551
719
|
Paddle::Notification.replay(id: "ntf_abc123")
|
|
720
|
+
#=> #<Paddle::Notification notification_id="ntf_abc456">
|
|
552
721
|
|
|
553
722
|
# List all logs for a notification
|
|
554
723
|
# https://developer.paddle.com/api-reference/notifications/list-notification-logs
|
|
@@ -580,9 +749,119 @@ Paddle::Report.create(
|
|
|
580
749
|
)
|
|
581
750
|
```
|
|
582
751
|
|
|
752
|
+
### Metrics
|
|
753
|
+
|
|
754
|
+
Daily metrics for your account. `from` and `to` are dates, as a string like `"2025-09-01"` or a `Date`.
|
|
755
|
+
`from` is inclusive and `to` is exclusive, and you can query up to 3 years in the past.
|
|
756
|
+
Your API key needs the `metrics.read` permission.
|
|
757
|
+
|
|
758
|
+
```ruby
|
|
759
|
+
# https://developer.paddle.com/api-reference/metrics/overview
|
|
760
|
+
metric = Paddle::Metric.monthly_recurring_revenue(from: "2025-09-01", to: "2025-09-05")
|
|
761
|
+
#=> #<Paddle::Metric interval="day", currency_code="USD", starts_at=..., ends_at=..., timeseries=[...]>
|
|
762
|
+
|
|
763
|
+
metric.timeseries.each do |point|
|
|
764
|
+
puts "#{point.timestamp}: #{point.amount}"
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
# Amounts are strings in the smallest currency unit, e.g. cents
|
|
768
|
+
Paddle::Metric.monthly_recurring_revenue_change(from: "2025-09-01", to: "2025-09-05") # amount
|
|
769
|
+
Paddle::Metric.revenue(from: "2025-09-01", to: "2025-09-05") # amount and count
|
|
770
|
+
Paddle::Metric.refunds(from: "2025-09-01", to: "2025-09-05") # amount
|
|
771
|
+
Paddle::Metric.active_subscribers(from: "2025-09-01", to: "2025-09-05") # count
|
|
772
|
+
Paddle::Metric.chargebacks(from: "2025-09-01", to: "2025-09-05") # count
|
|
773
|
+
Paddle::Metric.checkout_conversion(from: "2025-09-01", to: "2025-09-05") # count, completed_count and rate
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
#### Explore
|
|
777
|
+
|
|
778
|
+
Explore lets you run your own queries against your account data. Pick an entity and one or more measures,
|
|
779
|
+
then optionally filter the results and break them down by dimensions. Each combination of dimension values is a
|
|
780
|
+
segment, returned as one entry in `series`.
|
|
781
|
+
|
|
782
|
+
```ruby
|
|
783
|
+
# List the entities you can query, with their dimensions, measures and allowed intervals
|
|
784
|
+
# https://developer.paddle.com/api-reference/metrics/list-explore-metric-entities
|
|
785
|
+
Paddle::Metric.explore_entities
|
|
786
|
+
Paddle::Metric.explore_entities(entity: "transactions.completed")
|
|
787
|
+
|
|
788
|
+
# Run a query. from is inclusive and to is exclusive. interval can be day, week or month (the default)
|
|
789
|
+
# https://developer.paddle.com/api-reference/metrics/run-explore-metrics-query
|
|
790
|
+
result = Paddle::Metric.explore(
|
|
791
|
+
entity: "transactions.completed",
|
|
792
|
+
from: "2026-05-01",
|
|
793
|
+
to: "2026-08-01",
|
|
794
|
+
interval: "month",
|
|
795
|
+
measures: [ { field: "gross_revenue", agg: "sum" } ],
|
|
796
|
+
dimensions: [ "product" ],
|
|
797
|
+
order_by: [ { field: "gross_revenue", dir: "desc" } ],
|
|
798
|
+
filters: [ { field: "country", operator: "in", value: [ "GB", "US" ] } ]
|
|
799
|
+
)
|
|
800
|
+
#=> Paddle::ExploreResult
|
|
801
|
+
|
|
802
|
+
result.series.each do |segment|
|
|
803
|
+
puts segment.dimensions.product
|
|
804
|
+
segment.timeseries.each { |point| puts " #{point.timestamp}: #{point.measures.sum_gross_revenue}" }
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
# Results are paged by segment, 5 per page by default (max 50 using per_page)
|
|
808
|
+
result.has_more?
|
|
809
|
+
result.next_page
|
|
810
|
+
#=> Paddle::ExploreResult
|
|
811
|
+
|
|
812
|
+
# Iterate over every segment across all pages
|
|
813
|
+
result.auto_paging_each do |segment|
|
|
814
|
+
puts segment.dimensions.product
|
|
815
|
+
end
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
### Verifying Webhooks
|
|
819
|
+
|
|
820
|
+
Paddle signs every webhook it sends with a `Paddle-Signature` header. Verify it before trusting the payload.
|
|
821
|
+
You'll need the endpoint secret key for your notification destination, which is the `endpoint_secret_key` on its
|
|
822
|
+
`Paddle::NotificationSetting`, or in the dashboard under Developer Tools > Notifications.
|
|
823
|
+
|
|
824
|
+
The payload must be the **raw request body**. If it's parsed or reformatted first, the signature won't match.
|
|
825
|
+
|
|
826
|
+
```ruby
|
|
827
|
+
# https://developer.paddle.com/webhooks/signature-verification
|
|
828
|
+
class PaddleWebhooksController < ApplicationController
|
|
829
|
+
skip_forgery_protection
|
|
830
|
+
|
|
831
|
+
def create
|
|
832
|
+
event = Paddle::Webhook.construct_event(
|
|
833
|
+
payload: request.raw_post,
|
|
834
|
+
signature: request.headers["Paddle-Signature"],
|
|
835
|
+
secret: ENV["PADDLE_WEBHOOK_SECRET"]
|
|
836
|
+
)
|
|
837
|
+
#=> Paddle::Event
|
|
838
|
+
|
|
839
|
+
case event.event_type
|
|
840
|
+
when "transaction.completed"
|
|
841
|
+
# event.data.id, event.data.customer_id, ...
|
|
842
|
+
when "subscription.canceled"
|
|
843
|
+
# ...
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
head :ok
|
|
847
|
+
rescue Paddle::Webhook::SignatureVerificationError
|
|
848
|
+
head :bad_request
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
# Or just verify the signature. verify! returns true or raises
|
|
853
|
+
# Paddle::Webhook::SignatureVerificationError, and valid? returns true or false
|
|
854
|
+
Paddle::Webhook.verify!(payload: payload, signature: signature, secret: secret)
|
|
855
|
+
Paddle::Webhook.valid?(payload: payload, signature: signature, secret: secret)
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Webhooks are rejected if their timestamp is more than 5 seconds from the current time, to stop old requests being
|
|
859
|
+
replayed. You can change this with `tolerance:` (in seconds), or pass `tolerance: nil` to skip the check, for example
|
|
860
|
+
when testing with a stored webhook.
|
|
861
|
+
|
|
583
862
|
### Webhook Simulation Types
|
|
584
863
|
|
|
585
|
-
Retrieves a list of Simulation Types - https://developer.paddle.com/api-reference/simulation-types/overview
|
|
864
|
+
Retrieves a list of Simulation Types - <https://developer.paddle.com/api-reference/simulation-types/overview>
|
|
586
865
|
|
|
587
866
|
```ruby
|
|
588
867
|
Paddle::SimulationType.list
|
|
@@ -598,8 +877,8 @@ Paddle::Simulation.list(notification_setting_id: "nftset_abc123")
|
|
|
598
877
|
|
|
599
878
|
# Create a simulation
|
|
600
879
|
# https://developer.paddle.com/api-reference/simulations/create-simulation
|
|
601
|
-
Paddle::Simulation.create(
|
|
602
|
-
Paddle::Simulation.create(
|
|
880
|
+
Paddle::Simulation.create(setting_id: "ntfset_abc123", name: "Customer Create", type: "customer.completed")
|
|
881
|
+
Paddle::Simulation.create(setting_id: "ntfset_abc123", name: "Subscription Created", type: "subscription_creation")
|
|
603
882
|
|
|
604
883
|
# Retrieve a simulation
|
|
605
884
|
Paddle::Simulation.retrieve(id: "ntfsim_abc123")
|
|
@@ -611,6 +890,7 @@ Paddle::Simulation.update(id: "ntfsim_abc123", status: "archived")
|
|
|
611
890
|
|
|
612
891
|
# List all simulation runs
|
|
613
892
|
Paddle::Simulation.runs(id: "ntfsim_abc123")
|
|
893
|
+
Paddle::Simulation.runs(id: "ntfsim_abc123", per_page: 10, include: "events")
|
|
614
894
|
|
|
615
895
|
# Create a simulation run
|
|
616
896
|
# https://developer.paddle.com/api-reference/simulations/create-simulation-run
|
|
@@ -620,7 +900,11 @@ Paddle::SimulationRun.create(simulation_id: "ntfsim_abc123")
|
|
|
620
900
|
Paddle::SimulationRun.retrieve(simulation_id: "ntfsim_abc123", id: "ntfsimrun_abc123")
|
|
621
901
|
|
|
622
902
|
# List all simulation run events
|
|
623
|
-
Paddle::SimulationRun.events(simulation_id: "ntfsim_abc123",
|
|
903
|
+
Paddle::SimulationRun.events(simulation_id: "ntfsim_abc123", id: "ntfsimrun_abc123")
|
|
904
|
+
Paddle::SimulationRun.events(simulation_id: "ntfsim_abc123", id: "ntfsimrun_abc123", per_page: 10)
|
|
905
|
+
|
|
906
|
+
# Retrieve a simulation run event
|
|
907
|
+
Paddle::SimulationRunEvent.retrieve(simulation_id: "ntfsim_abc123", run_id: "ntfsimrun_abc123", id: "ntfsimevt_abc123")
|
|
624
908
|
|
|
625
909
|
# Replay a simulation run event
|
|
626
910
|
# https://developer.paddle.com/api-reference/simulations/replay-simulation-run-event
|
|
@@ -647,65 +931,9 @@ Paddle::ClientToken.retrieve id: "ctkn_abc123"
|
|
|
647
931
|
Paddle::ClientToken.update id: "ctkn_abc123", status: "revoked"
|
|
648
932
|
```
|
|
649
933
|
|
|
650
|
-
|
|
651
|
-
## Classic API
|
|
652
|
-
|
|
653
|
-
For accessing the Paddle Classic API
|
|
654
|
-
|
|
655
|
-
### Set Client Details
|
|
656
|
-
|
|
657
|
-
Firstly you'll need to set your Vendor ID, Vendor Auth Code and if you want
|
|
658
|
-
to use the Sandbox API or not.
|
|
659
|
-
|
|
660
|
-
You can find your vendor details [here for production](https://vendors.paddle.com/authentication),
|
|
661
|
-
or [here for sandbox](https://sandbox-vendors.paddle.com/authentication)
|
|
662
|
-
|
|
663
|
-
```ruby
|
|
664
|
-
@client = Paddle::Classic::Client.new(
|
|
665
|
-
vendor_id: "",
|
|
666
|
-
vendor_auth_code: "",
|
|
667
|
-
# Use the sandbox version of the API
|
|
668
|
-
sandbox: true
|
|
669
|
-
)
|
|
670
|
-
```
|
|
671
|
-
|
|
672
|
-
### Plans
|
|
673
|
-
|
|
674
|
-
```ruby
|
|
675
|
-
# Retrieves a list of Plans
|
|
676
|
-
@client.plans.list
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
### Subscription Users
|
|
680
|
-
|
|
681
|
-
```ruby
|
|
682
|
-
# List all users subscribed to any plan
|
|
683
|
-
@client.users.list
|
|
684
|
-
@client.users.list(subscription_id: "abc123")
|
|
685
|
-
@client.users.list(plan_id: "abc123")
|
|
686
|
-
@client.users.list(state: "active")
|
|
687
|
-
@client.users.list(state: "deleted")
|
|
688
|
-
|
|
689
|
-
# Update a user's subscription
|
|
690
|
-
# https://developer.paddle.com/api-reference/e3872343dfbba-update-user
|
|
691
|
-
@client.users.update(subscription_id: "abc123")
|
|
692
|
-
|
|
693
|
-
# Pause a user's subscription
|
|
694
|
-
@client.users.pause(subscription_id: "abc123")
|
|
695
|
-
|
|
696
|
-
# Unpause a user's subscription
|
|
697
|
-
@client.users.unpause(subscription_id: "abc123")
|
|
698
|
-
|
|
699
|
-
# Update the Postcode/ZIP Code of a user's subscription
|
|
700
|
-
@client.users.update_postcode(subscription_id: "abc123", postcode: "123abc")
|
|
701
|
-
|
|
702
|
-
# Cancel a user's subscription
|
|
703
|
-
@client.users.cancel(subscription_id: "abc123")
|
|
704
|
-
```
|
|
705
|
-
|
|
706
934
|
## Contributing
|
|
707
935
|
|
|
708
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
|
936
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/d34ndev/paddle>.
|
|
709
937
|
|
|
710
938
|
## License
|
|
711
939
|
|
|
@@ -71,10 +71,10 @@ module Paddle
|
|
|
71
71
|
def connection
|
|
72
72
|
@connection ||= Faraday.new(url) do |conn|
|
|
73
73
|
conn.headers = {
|
|
74
|
-
"User-Agent" => "paddle/v#{VERSION} (github.com/
|
|
74
|
+
"User-Agent" => "paddle/v#{VERSION} (github.com/d34ndev/paddle)"
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
conn.request :
|
|
77
|
+
conn.request :url_encoded
|
|
78
78
|
|
|
79
79
|
conn.response :json, content_type: "application/json"
|
|
80
80
|
|