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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cc578ecefeacc14fa58ae9fc433429c3180d5c5bc6015493bc66afc50172cccd
4
- data.tar.gz: 03d85b6ee1f8fedb0c5b56028dfb741d32155471bfda087b7cf61bd2872f60ef
3
+ metadata.gz: ae96654d26047a02fed95576d33befa70e04ae3103e4f191de9f8a8a6e71bdb8
4
+ data.tar.gz: 3e434eabe578af9f29e9fd543b829a691f8afc8b2c6f4cb2fbf1f9334a75ea97
5
5
  SHA512:
6
- metadata.gz: 4ecdcde91d5d11a03c7f5e1506f923b3cfd738bbc70113436891768e2cdbdc194694a67e9e452256b6d8be5918b0eed315ce27ab9e5a7a5920312023f3ed3acc
7
- data.tar.gz: 851daa7f4a126c94891cf136b80761f7a98745cd3a21455cadb8af1e7f782f3972a8e0123585320052d6335bd25598706e212e1224aaabd93c18c100f195c16b
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", "~> 2.8"
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
- type of object you're working with. They're built using OpenStruct so you can easily access data in a Ruby-ish way.
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
- # Retrieve credit balance for a customer
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/transaction/update-transaction
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/transaction/preview-transaction
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/transaction/get-invoice-pdf
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: "txnitm_anc123"
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
- # Attempts to resend a notification
550
- # (currently not working)
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(notification_setting_id: "ntfset_abc123", name: "Customer Create", type: "customer.completed")
602
- Paddle::Simulation.create(notification_setting_id: "ntfset_abc123", name: "Subscription Created", type: "subscription_creation")
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", run_id: "ntfsimrun_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/deanpcmad/paddle.
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/deanpcmad/paddle)"
74
+ "User-Agent" => "paddle/v#{VERSION} (github.com/d34ndev/paddle)"
75
75
  }
76
76
 
77
- conn.request :json
77
+ conn.request :url_encoded
78
78
 
79
79
  conn.response :json, content_type: "application/json"
80
80