paddle 2.10 → 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 +284 -70
- data/lib/paddle/classic/client.rb +1 -1
- data/lib/paddle/client.rb +31 -15
- data/lib/paddle/collection.rb +47 -11
- data/lib/paddle/configuration.rb +66 -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 +20 -48
- 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,6 +35,41 @@ 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
|
+
|
|
36
73
|
### Connection Options
|
|
37
74
|
|
|
38
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:
|
|
@@ -52,8 +89,21 @@ end
|
|
|
52
89
|
|
|
53
90
|
The gem maps as closely as we can to the Paddle API so you can easily convert API examples to gem code.
|
|
54
91
|
|
|
55
|
-
Responses are created as objects like `Paddle::Product`. Having types like `Paddle::Product` is handy for understanding what
|
|
56
|
-
|
|
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
|
+
```
|
|
57
107
|
|
|
58
108
|
### Pagination
|
|
59
109
|
|
|
@@ -66,8 +116,14 @@ results = Paddle::Product.list(per_page: 10)
|
|
|
66
116
|
#=> Paddle::Collection
|
|
67
117
|
|
|
68
118
|
results.total
|
|
119
|
+
#=> 42
|
|
120
|
+
|
|
121
|
+
results.per_page
|
|
69
122
|
#=> 10
|
|
70
123
|
|
|
124
|
+
results.has_more?
|
|
125
|
+
#=> true
|
|
126
|
+
|
|
71
127
|
results.data
|
|
72
128
|
#=> [#<Paddle::Product>, #<Paddle::Product>]
|
|
73
129
|
|
|
@@ -81,9 +137,37 @@ results.first
|
|
|
81
137
|
results.last
|
|
82
138
|
#=> #<Paddle::Product>
|
|
83
139
|
|
|
84
|
-
# 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
|
|
85
145
|
Paddle::Product.list(per_page: 10, after: "abc123")
|
|
86
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
|
|
87
171
|
```
|
|
88
172
|
|
|
89
173
|
### Caveats
|
|
@@ -92,6 +176,14 @@ Paddle::Product.list(per_page: 10, after: "abc123")
|
|
|
92
176
|
>
|
|
93
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.
|
|
94
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
|
+
|
|
95
187
|
### Error Handling
|
|
96
188
|
|
|
97
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.
|
|
@@ -116,6 +208,7 @@ All errors inherit from `Paddle::ErrorGenerator` and include:
|
|
|
116
208
|
- `Paddle::Errors::ConflictError` (409) - Request conflicts with existing data
|
|
117
209
|
- `Paddle::Errors::TooManyRequestsError` (429) - Rate limit exceeded
|
|
118
210
|
- `Paddle::Errors::InternalError` (500) - Server error
|
|
211
|
+
- `Paddle::Errors::NotImplementedError` (501) - Resource not implemented
|
|
119
212
|
- `Paddle::Errors::ServiceUnavailableError` (503) - Service unavailable
|
|
120
213
|
|
|
121
214
|
#### Error Example
|
|
@@ -291,8 +384,12 @@ customer.update(status: "archived")
|
|
|
291
384
|
# or
|
|
292
385
|
Paddle::Customer.update(id: "ctm_abc123", status: "archived")
|
|
293
386
|
|
|
294
|
-
#
|
|
387
|
+
# List credit balances for a customer. Customers have a balance for each currency
|
|
295
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
|
|
296
393
|
Paddle::Customer.credit(id: "ctm_abc123")
|
|
297
394
|
|
|
298
395
|
# Generate an authentication token for a customer
|
|
@@ -364,21 +461,32 @@ Paddle::Transaction.retrieve(id: "txn_abc123")
|
|
|
364
461
|
transaction = Paddle::Transaction.retrieve(id: "txn_abc123", extra: "customer")
|
|
365
462
|
|
|
366
463
|
# Update a transaction
|
|
367
|
-
# https://developer.paddle.com/api-reference/
|
|
464
|
+
# https://developer.paddle.com/api-reference/transactions/update-transaction
|
|
368
465
|
transaction.update(items: [ { price_id: "pri_abc123", quantity: 2 } ])
|
|
369
466
|
# or
|
|
370
467
|
Paddle::Transaction.update(id: "txn_abc123", items: [ { price_id: "pri_abc123", quantity: 2 } ])
|
|
371
468
|
|
|
372
469
|
# Preview a transaction
|
|
373
|
-
# https://developer.paddle.com/api-reference/
|
|
470
|
+
# https://developer.paddle.com/api-reference/transactions/preview-transaction
|
|
374
471
|
Paddle::Transaction.preview(items: [ { price_id: "pri_123abc", quantity: 5 } ])
|
|
375
472
|
|
|
376
473
|
# Get a PDF invoice for a transaction
|
|
377
474
|
# disposition defaults to "attachment"
|
|
378
475
|
# Returns a raw URL. This URL is not permanent and will expire.
|
|
379
|
-
# https://developer.paddle.com/api-reference/
|
|
476
|
+
# https://developer.paddle.com/api-reference/transactions/get-invoice-pdf
|
|
380
477
|
Paddle::Transaction.invoice(id: "txn_abc123", disposition: "inline")
|
|
381
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
|
+
)
|
|
382
490
|
```
|
|
383
491
|
|
|
384
492
|
### Subscriptions
|
|
@@ -418,6 +526,11 @@ Paddle::Subscription.get_transaction(id: "sub_abc123")
|
|
|
418
526
|
# https://developer.paddle.com/api-reference/subscriptions/create-one-time-charge
|
|
419
527
|
Paddle::Subscription.charge(id: "sub_abc123", items: [ { price_id: "pri_123abc", quantity: 2 } ], effective_from: "immediately")
|
|
420
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
|
+
|
|
421
534
|
# Pause a subscription
|
|
422
535
|
# https://developer.paddle.com/api-reference/subscriptions/pause-subscription
|
|
423
536
|
Paddle::Subscription.pause(id: "sub_abc123")
|
|
@@ -437,6 +550,18 @@ Paddle::Subscription.cancel(id: "sub_abc123", effective_from: "immediately")
|
|
|
437
550
|
# Activate a trialing subscription
|
|
438
551
|
# https://developer.paddle.com/api-reference/subscriptions/activate-subscription
|
|
439
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
|
|
440
565
|
```
|
|
441
566
|
|
|
442
567
|
### Customer Portal Sessions
|
|
@@ -466,11 +591,38 @@ Paddle::Adjustment.create(
|
|
|
466
591
|
items: [
|
|
467
592
|
{
|
|
468
593
|
type: "full",
|
|
469
|
-
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"
|
|
470
610
|
}
|
|
471
611
|
]
|
|
472
612
|
)
|
|
473
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
|
+
|
|
474
626
|
# Get a credit note for an adjustment
|
|
475
627
|
# disposition defaults to "attachment"
|
|
476
628
|
# Returns a raw URL. This URL is not permanent and will expire.
|
|
@@ -561,9 +713,11 @@ Paddle::Notification.list(status: "failed")
|
|
|
561
713
|
Paddle::Notification.retrieve(id: "ntf_abc123")
|
|
562
714
|
|
|
563
715
|
# Replay a notification
|
|
564
|
-
#
|
|
565
|
-
#
|
|
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
|
|
566
719
|
Paddle::Notification.replay(id: "ntf_abc123")
|
|
720
|
+
#=> #<Paddle::Notification notification_id="ntf_abc456">
|
|
567
721
|
|
|
568
722
|
# List all logs for a notification
|
|
569
723
|
# https://developer.paddle.com/api-reference/notifications/list-notification-logs
|
|
@@ -595,6 +749,116 @@ Paddle::Report.create(
|
|
|
595
749
|
)
|
|
596
750
|
```
|
|
597
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
|
+
|
|
598
862
|
### Webhook Simulation Types
|
|
599
863
|
|
|
600
864
|
Retrieves a list of Simulation Types - <https://developer.paddle.com/api-reference/simulation-types/overview>
|
|
@@ -613,8 +877,8 @@ Paddle::Simulation.list(notification_setting_id: "nftset_abc123")
|
|
|
613
877
|
|
|
614
878
|
# Create a simulation
|
|
615
879
|
# https://developer.paddle.com/api-reference/simulations/create-simulation
|
|
616
|
-
Paddle::Simulation.create(
|
|
617
|
-
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")
|
|
618
882
|
|
|
619
883
|
# Retrieve a simulation
|
|
620
884
|
Paddle::Simulation.retrieve(id: "ntfsim_abc123")
|
|
@@ -626,6 +890,7 @@ Paddle::Simulation.update(id: "ntfsim_abc123", status: "archived")
|
|
|
626
890
|
|
|
627
891
|
# List all simulation runs
|
|
628
892
|
Paddle::Simulation.runs(id: "ntfsim_abc123")
|
|
893
|
+
Paddle::Simulation.runs(id: "ntfsim_abc123", per_page: 10, include: "events")
|
|
629
894
|
|
|
630
895
|
# Create a simulation run
|
|
631
896
|
# https://developer.paddle.com/api-reference/simulations/create-simulation-run
|
|
@@ -635,7 +900,11 @@ Paddle::SimulationRun.create(simulation_id: "ntfsim_abc123")
|
|
|
635
900
|
Paddle::SimulationRun.retrieve(simulation_id: "ntfsim_abc123", id: "ntfsimrun_abc123")
|
|
636
901
|
|
|
637
902
|
# List all simulation run events
|
|
638
|
-
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")
|
|
639
908
|
|
|
640
909
|
# Replay a simulation run event
|
|
641
910
|
# https://developer.paddle.com/api-reference/simulations/replay-simulation-run-event
|
|
@@ -662,64 +931,9 @@ Paddle::ClientToken.retrieve id: "ctkn_abc123"
|
|
|
662
931
|
Paddle::ClientToken.update id: "ctkn_abc123", status: "revoked"
|
|
663
932
|
```
|
|
664
933
|
|
|
665
|
-
## Classic API
|
|
666
|
-
|
|
667
|
-
For accessing the Paddle Classic API
|
|
668
|
-
|
|
669
|
-
### Set Client Details
|
|
670
|
-
|
|
671
|
-
Firstly you'll need to set your Vendor ID, Vendor Auth Code and if you want
|
|
672
|
-
to use the Sandbox API or not.
|
|
673
|
-
|
|
674
|
-
You can find your vendor details [here for production](https://vendors.paddle.com/authentication),
|
|
675
|
-
or [here for sandbox](https://sandbox-vendors.paddle.com/authentication)
|
|
676
|
-
|
|
677
|
-
```ruby
|
|
678
|
-
@client = Paddle::Classic::Client.new(
|
|
679
|
-
vendor_id: "",
|
|
680
|
-
vendor_auth_code: "",
|
|
681
|
-
# Use the sandbox version of the API
|
|
682
|
-
sandbox: true
|
|
683
|
-
)
|
|
684
|
-
```
|
|
685
|
-
|
|
686
|
-
### Plans
|
|
687
|
-
|
|
688
|
-
```ruby
|
|
689
|
-
# Retrieves a list of Plans
|
|
690
|
-
@client.plans.list
|
|
691
|
-
```
|
|
692
|
-
|
|
693
|
-
### Subscription Users
|
|
694
|
-
|
|
695
|
-
```ruby
|
|
696
|
-
# List all users subscribed to any plan
|
|
697
|
-
@client.users.list
|
|
698
|
-
@client.users.list(subscription_id: "abc123")
|
|
699
|
-
@client.users.list(plan_id: "abc123")
|
|
700
|
-
@client.users.list(state: "active")
|
|
701
|
-
@client.users.list(state: "deleted")
|
|
702
|
-
|
|
703
|
-
# Update a user's subscription
|
|
704
|
-
# https://developer.paddle.com/api-reference/e3872343dfbba-update-user
|
|
705
|
-
@client.users.update(subscription_id: "abc123")
|
|
706
|
-
|
|
707
|
-
# Pause a user's subscription
|
|
708
|
-
@client.users.pause(subscription_id: "abc123")
|
|
709
|
-
|
|
710
|
-
# Unpause a user's subscription
|
|
711
|
-
@client.users.unpause(subscription_id: "abc123")
|
|
712
|
-
|
|
713
|
-
# Update the Postcode/ZIP Code of a user's subscription
|
|
714
|
-
@client.users.update_postcode(subscription_id: "abc123", postcode: "123abc")
|
|
715
|
-
|
|
716
|
-
# Cancel a user's subscription
|
|
717
|
-
@client.users.cancel(subscription_id: "abc123")
|
|
718
|
-
```
|
|
719
|
-
|
|
720
934
|
## Contributing
|
|
721
935
|
|
|
722
|
-
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>.
|
|
723
937
|
|
|
724
938
|
## License
|
|
725
939
|
|
|
@@ -71,7 +71,7 @@ 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
77
|
conn.request :url_encoded
|