polar-ruby 0.1.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.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/CHANGELOG.md +50 -0
  4. data/DEVELOPMENT.md +329 -0
  5. data/EXAMPLES.md +385 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +115 -0
  8. data/LICENSE +23 -0
  9. data/PROJECT_SUMMARY.md +256 -0
  10. data/README.md +635 -0
  11. data/Rakefile +24 -0
  12. data/examples/demo.rb +106 -0
  13. data/lib/polar/authentication.rb +83 -0
  14. data/lib/polar/client.rb +144 -0
  15. data/lib/polar/configuration.rb +46 -0
  16. data/lib/polar/customer_portal/benefit_grants.rb +41 -0
  17. data/lib/polar/customer_portal/customers.rb +69 -0
  18. data/lib/polar/customer_portal/license_keys.rb +70 -0
  19. data/lib/polar/customer_portal/orders.rb +82 -0
  20. data/lib/polar/customer_portal/subscriptions.rb +51 -0
  21. data/lib/polar/errors.rb +96 -0
  22. data/lib/polar/http_client.rb +150 -0
  23. data/lib/polar/pagination.rb +133 -0
  24. data/lib/polar/resources/base.rb +47 -0
  25. data/lib/polar/resources/benefits.rb +64 -0
  26. data/lib/polar/resources/checkouts.rb +75 -0
  27. data/lib/polar/resources/customers.rb +120 -0
  28. data/lib/polar/resources/events.rb +45 -0
  29. data/lib/polar/resources/files.rb +57 -0
  30. data/lib/polar/resources/license_keys.rb +81 -0
  31. data/lib/polar/resources/metrics.rb +30 -0
  32. data/lib/polar/resources/oauth2.rb +61 -0
  33. data/lib/polar/resources/orders.rb +54 -0
  34. data/lib/polar/resources/organizations.rb +41 -0
  35. data/lib/polar/resources/payments.rb +29 -0
  36. data/lib/polar/resources/products.rb +58 -0
  37. data/lib/polar/resources/subscriptions.rb +55 -0
  38. data/lib/polar/resources/webhooks.rb +81 -0
  39. data/lib/polar/version.rb +5 -0
  40. data/lib/polar/webhooks.rb +174 -0
  41. data/lib/polar.rb +65 -0
  42. metadata +239 -0
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polar
4
+ # Base error class for all Polar SDK errors
5
+ class Error < StandardError
6
+ attr_reader :status_code, :headers, :body
7
+
8
+ def initialize(message, status_code: nil, headers: {}, body: nil)
9
+ super(message)
10
+ @status_code = status_code
11
+ @headers = headers || {}
12
+ @body = body
13
+ end
14
+ end
15
+
16
+ # HTTP error base class
17
+ class HTTPError < Error
18
+ def initialize(response)
19
+ @status_code = response.status
20
+ @headers = response.headers
21
+ @body = response.body
22
+
23
+ message = parse_error_message(response)
24
+ super(message, status_code: @status_code, headers: @headers, body: @body)
25
+ end
26
+
27
+ private
28
+
29
+ def parse_error_message(response)
30
+ return "HTTP #{response.status}" unless response.body
31
+
32
+ begin
33
+ # Handle both already parsed JSON (Hash) and raw JSON string
34
+ parsed = response.body.is_a?(Hash) ? response.body : JSON.parse(response.body)
35
+ parsed['detail'] || parsed['message'] || "HTTP #{response.status}"
36
+ rescue JSON::ParserError, TypeError
37
+ "HTTP #{response.status}"
38
+ end
39
+ end
40
+ end
41
+
42
+ # Specific HTTP error classes matching Polar API responses
43
+ class BadRequestError < HTTPError; end
44
+ class UnauthorizedError < HTTPError; end
45
+ class ForbiddenError < HTTPError; end
46
+ class NotFoundError < HTTPError; end
47
+ class MethodNotAllowedError < HTTPError; end
48
+ class UnprocessableEntityError < HTTPError; end
49
+ class TooManyRequestsError < HTTPError; end
50
+ class InternalServerError < HTTPError; end
51
+ class BadGatewayError < HTTPError; end
52
+ class ServiceUnavailableError < HTTPError; end
53
+ class GatewayTimeoutError < HTTPError; end
54
+
55
+ # Specific Polar API errors
56
+ class ValidationError < UnprocessableEntityError; end
57
+ class ResourceNotFoundError < NotFoundError; end
58
+ class NotPermittedError < ForbiddenError; end
59
+ class AlreadyCanceledSubscriptionError < ForbiddenError; end
60
+ class AlreadyActiveSubscriptionError < ForbiddenError; end
61
+
62
+ # Connection and timeout errors
63
+ class ConnectionError < Error; end
64
+ class TimeoutError < Error; end
65
+ class RetryExhaustedError < Error; end
66
+
67
+ # Authentication errors
68
+ class AuthenticationError < Error; end
69
+ class InvalidTokenError < AuthenticationError; end
70
+
71
+ # Webhook errors
72
+ class WebhookError < Error; end
73
+ class WebhookVerificationError < WebhookError; end
74
+
75
+ class << self
76
+ # Map HTTP status codes to error classes
77
+ STATUS_CODE_TO_ERROR = {
78
+ 400 => BadRequestError,
79
+ 401 => UnauthorizedError,
80
+ 403 => ForbiddenError,
81
+ 404 => NotFoundError,
82
+ 405 => MethodNotAllowedError,
83
+ 422 => UnprocessableEntityError,
84
+ 429 => TooManyRequestsError,
85
+ 500 => InternalServerError,
86
+ 502 => BadGatewayError,
87
+ 503 => ServiceUnavailableError,
88
+ 504 => GatewayTimeoutError
89
+ }.freeze
90
+
91
+ def error_for_status(response)
92
+ error_class = STATUS_CODE_TO_ERROR[response.status] || HTTPError
93
+ error_class.new(response)
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polar
4
+ class HTTPClient
5
+ attr_reader :configuration, :auth
6
+
7
+ def initialize(configuration, auth = nil)
8
+ @configuration = configuration
9
+ @auth = auth
10
+ end
11
+
12
+ def get(path, params = {}, headers = {})
13
+ request(:get, path, params: params, headers: headers)
14
+ end
15
+
16
+ def post(path, body = nil, headers = {})
17
+ request(:post, path, body: body, headers: headers)
18
+ end
19
+
20
+ def put(path, body = nil, headers = {})
21
+ request(:put, path, body: body, headers: headers)
22
+ end
23
+
24
+ def patch(path, body = nil, headers = {})
25
+ request(:patch, path, body: body, headers: headers)
26
+ end
27
+
28
+ def delete(path, headers = {})
29
+ request(:delete, path, headers: headers)
30
+ end
31
+
32
+ private
33
+
34
+ def request(method, path, params: nil, body: nil, headers: {})
35
+ url = build_url(path)
36
+ request_headers = build_headers(headers)
37
+
38
+ log_request(method, url, body) if configuration.debug
39
+
40
+ response = connection.public_send(method) do |req|
41
+ req.url url
42
+ req.headers.update(request_headers)
43
+ params.each { |key, value| req.params[key.to_s] = value } if params && method == :get
44
+ req.body = prepare_body(body) if body && %i[post put patch].include?(method)
45
+ end
46
+
47
+ log_response(response) if configuration.debug
48
+
49
+ handle_response(response)
50
+ rescue Faraday::Error => e
51
+ handle_faraday_error(e)
52
+ end
53
+
54
+ def connection
55
+ @connection ||= Faraday.new do |conn|
56
+ conn.request :json
57
+ conn.request :multipart
58
+ conn.request :retry, retry_options
59
+ conn.response :json, content_type: /\bjson$/
60
+ conn.options.timeout = configuration.timeout
61
+ conn.adapter Faraday.default_adapter
62
+ end
63
+ end
64
+
65
+ def retry_options
66
+ {
67
+ max: configuration.retries,
68
+ interval: 0.5,
69
+ interval_randomness: 0.5,
70
+ backoff_factor: 2,
71
+ exceptions: [
72
+ Faraday::ConnectionFailed,
73
+ Faraday::TimeoutError,
74
+ Faraday::RetriableResponse
75
+ ],
76
+ retry_statuses: [429, 500, 502, 503, 504],
77
+ retry_if: ->(env, _exception) { env.method != :post || retriable_post?(env) }
78
+ }
79
+ end
80
+
81
+ def retriable_post?(env)
82
+ # Only retry POST requests that are idempotent (like searches)
83
+ env.url.path.include?('/search') || env.url.path.include?('/validate')
84
+ end
85
+
86
+ def build_url(path)
87
+ path.start_with?('http') ? path : "#{configuration.base_url}#{path}"
88
+ end
89
+
90
+ def build_headers(headers)
91
+ default_headers = {
92
+ 'User-Agent' => "polar-ruby/#{Polar::VERSION}",
93
+ 'Accept' => 'application/json',
94
+ 'Content-Type' => 'application/json'
95
+ }
96
+
97
+ default_headers.merge!(auth.headers) if auth
98
+ default_headers.merge(headers)
99
+ end
100
+
101
+ def prepare_body(body)
102
+ case body
103
+ when Hash, Array
104
+ body.to_json
105
+ when String
106
+ body
107
+ else
108
+ body.to_s
109
+ end
110
+ end
111
+
112
+ def handle_response(response)
113
+ case response.status
114
+ when 200..299
115
+ response
116
+ when 400..499, 500..599
117
+ raise Polar.error_for_status(response)
118
+ else
119
+ raise HTTPError.new(response)
120
+ end
121
+ end
122
+
123
+ def handle_faraday_error(error)
124
+ case error
125
+ when Faraday::ConnectionFailed
126
+ raise ConnectionError, "Failed to connect to Polar API: #{error.message}"
127
+ when Faraday::TimeoutError
128
+ raise TimeoutError, "Request timed out: #{error.message}"
129
+ when Faraday::RetriableResponse
130
+ raise RetryExhaustedError, "Request failed after #{configuration.retries} retries: #{error.message}"
131
+ else
132
+ raise Error, "Network error: #{error.message}"
133
+ end
134
+ end
135
+
136
+ def log_request(method, url, body)
137
+ return unless configuration.logger
138
+
139
+ configuration.logger.debug("Polar API Request: #{method.upcase} #{url}")
140
+ configuration.logger.debug("Request Body: #{body}") if body
141
+ end
142
+
143
+ def log_response(response)
144
+ return unless configuration.logger
145
+
146
+ configuration.logger.debug("Polar API Response: #{response.status}")
147
+ configuration.logger.debug("Response Body: #{response.body}") if response.body
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polar
4
+ class PaginatedResponse
5
+ include Enumerable
6
+
7
+ attr_reader :client, :path, :params, :headers, :current_page, :total_count
8
+
9
+ def initialize(client, path, params = {}, headers = {})
10
+ @client = client
11
+ @path = path
12
+ @params = params.dup
13
+ @headers = headers
14
+ @current_page = nil
15
+ @total_count = nil
16
+ @items_cache = {}
17
+ end
18
+
19
+ def each(&block)
20
+ return enum_for(:each) unless block_given?
21
+
22
+ page = 1
23
+ loop do
24
+ response_data = fetch_page(page)
25
+ items = extract_items(response_data)
26
+
27
+ break if items.empty?
28
+
29
+ items.each(&block)
30
+
31
+ # Check if there are more pages
32
+ break unless has_next_page?(response_data, page)
33
+
34
+ page += 1
35
+ end
36
+ end
37
+
38
+ def auto_paginate
39
+ all_items = []
40
+ each { |item| all_items << item }
41
+ all_items
42
+ end
43
+
44
+ def first_page
45
+ @first_page ||= fetch_page(1)
46
+ end
47
+
48
+ def page(page_number)
49
+ fetch_page(page_number)
50
+ end
51
+
52
+ def count
53
+ first_page['pagination']['total'] if first_page.dig('pagination')
54
+ end
55
+
56
+ def total_pages
57
+ pagination_info = first_page.dig('pagination')
58
+ return nil unless pagination_info
59
+
60
+ total = pagination_info['total']
61
+ per_page = pagination_info['per_page'] || pagination_info['limit'] || 20
62
+ (total.to_f / per_page).ceil
63
+ end
64
+
65
+ private
66
+
67
+ def fetch_page(page_number)
68
+ return @items_cache[page_number] if @items_cache[page_number]
69
+
70
+ page_params = @params.merge(page: page_number)
71
+ response = @client.http_client.get(@path, page_params, @headers)
72
+
73
+ response_data = response.body
74
+ @items_cache[page_number] = response_data
75
+ @current_page = page_number
76
+
77
+ response_data
78
+ end
79
+
80
+ def extract_items(response_data)
81
+ # The response could have different structures
82
+ if response_data.is_a?(Hash)
83
+ # Check for common pagination response structures
84
+ if response_data.key?('data')
85
+ response_data['data']
86
+ elsif response_data.key?('items')
87
+ response_data['items']
88
+ elsif response_data.key?('results')
89
+ response_data['results']
90
+ else
91
+ # If it's a hash but no data key, it might be the items themselves
92
+ []
93
+ end
94
+ elsif response_data.is_a?(Array)
95
+ response_data
96
+ else
97
+ []
98
+ end
99
+ end
100
+
101
+ def has_next_page?(response_data, current_page)
102
+ return false unless response_data.is_a?(Hash)
103
+
104
+ pagination = response_data['pagination']
105
+ return false unless pagination
106
+
107
+ # Check various pagination indicators
108
+ if pagination.key?('has_next')
109
+ pagination['has_next']
110
+ elsif pagination.key?('next_page')
111
+ !pagination['next_page'].nil?
112
+ elsif pagination.key?('total')
113
+ total = pagination['total']
114
+ per_page = pagination['per_page'] || pagination['limit'] || 20
115
+ current_page * per_page < total
116
+ else
117
+ # If no clear pagination info, check if we got any items
118
+ items = extract_items(response_data)
119
+ !items.empty?
120
+ end
121
+ end
122
+ end
123
+
124
+ module Pagination
125
+ def paginate(path, params = {}, headers = {})
126
+ PaginatedResponse.new(self, path, params, headers)
127
+ end
128
+
129
+ def auto_paginate(path, params = {}, headers = {})
130
+ paginate(path, params, headers).auto_paginate
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Polar
4
+ module Resources
5
+ class Base
6
+ attr_reader :client
7
+
8
+ def initialize(client)
9
+ @client = client
10
+ end
11
+
12
+ protected
13
+
14
+ def http_client
15
+ client.http_client
16
+ end
17
+
18
+ def get(path, params = {}, headers = {})
19
+ http_client.get(path, params, headers)
20
+ end
21
+
22
+ def post(path, body = nil, headers = {})
23
+ http_client.post(path, body, headers)
24
+ end
25
+
26
+ def put(path, body = nil, headers = {})
27
+ http_client.put(path, body, headers)
28
+ end
29
+
30
+ def patch(path, body = nil, headers = {})
31
+ http_client.patch(path, body, headers)
32
+ end
33
+
34
+ def delete(path, headers = {})
35
+ http_client.delete(path, headers)
36
+ end
37
+
38
+ def paginate(path, params = {}, headers = {})
39
+ client.paginate(path, params, headers)
40
+ end
41
+
42
+ def auto_paginate(path, params = {}, headers = {})
43
+ client.auto_paginate(path, params, headers)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Polar
6
+ module Resources
7
+ class Benefits < Base
8
+ # List benefits
9
+ # @param params [Hash] Query parameters
10
+ # @option params [String] :organization_id Filter by organization ID
11
+ # @option params [String] :type Filter by benefit type
12
+ # @option params [Integer] :page Page number
13
+ # @option params [Integer] :limit Items per page
14
+ # @return [PaginatedResponse] Paginated list of benefits
15
+ def list(params = {})
16
+ paginate('/benefits/', params)
17
+ end
18
+
19
+ # Create a benefit
20
+ # @param attributes [Hash] Benefit attributes
21
+ # @option attributes [String] :type Benefit type
22
+ # @option attributes [String] :description Benefit description
23
+ # @option attributes [String] :organization_id Organization ID
24
+ # @return [Hash] Created benefit
25
+ def create(attributes)
26
+ response = post('/benefits/', attributes)
27
+ response.body
28
+ end
29
+
30
+ # Get a benefit by ID
31
+ # @param id [String] Benefit ID
32
+ # @return [Hash] Benefit data
33
+ def get(id)
34
+ response = get("/benefits/#{id}")
35
+ response.body
36
+ end
37
+
38
+ # Update a benefit
39
+ # @param id [String] Benefit ID
40
+ # @param attributes [Hash] Updated attributes
41
+ # @return [Hash] Updated benefit
42
+ def update(id, attributes)
43
+ response = patch("/benefits/#{id}", attributes)
44
+ response.body
45
+ end
46
+
47
+ # Delete a benefit
48
+ # @param id [String] Benefit ID
49
+ # @return [Boolean] Success status
50
+ def delete(id)
51
+ delete("/benefits/#{id}")
52
+ true
53
+ end
54
+
55
+ # List benefit grants
56
+ # @param id [String] Benefit ID
57
+ # @param params [Hash] Query parameters
58
+ # @return [PaginatedResponse] Paginated list of benefit grants
59
+ def grants(id, params = {})
60
+ paginate("/benefits/#{id}/grants", params)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Polar
6
+ module Resources
7
+ class Checkouts < Base
8
+ # List checkout sessions
9
+ # @param params [Hash] Query parameters
10
+ # @option params [String] :organization_id Filter by organization ID
11
+ # @option params [String] :customer_id Filter by customer ID
12
+ # @option params [Integer] :page Page number
13
+ # @option params [Integer] :limit Items per page
14
+ # @return [PaginatedResponse] Paginated list of checkout sessions
15
+ def list(params = {})
16
+ paginate('/checkouts/', params)
17
+ end
18
+
19
+ # Create a checkout session
20
+ # @param attributes [Hash] Checkout session attributes
21
+ # @option attributes [String] :product_price_id Product price ID
22
+ # @option attributes [String] :success_url Success redirect URL
23
+ # @option attributes [String] :cancel_url Cancel redirect URL
24
+ # @option attributes [Hash] :customer_data Customer information
25
+ # @return [Hash] Created checkout session
26
+ def create(attributes)
27
+ response = post('/checkouts/', attributes)
28
+ response.body
29
+ end
30
+
31
+ # Get a checkout session by ID
32
+ # @param id [String] Checkout session ID
33
+ # @return [Hash] Checkout session data
34
+ def get(id)
35
+ response = get("/checkouts/#{id}")
36
+ response.body
37
+ end
38
+
39
+ # Update a checkout session
40
+ # @param id [String] Checkout session ID
41
+ # @param attributes [Hash] Updated attributes
42
+ # @return [Hash] Updated checkout session
43
+ def update(id, attributes)
44
+ response = patch("/checkouts/#{id}", attributes)
45
+ response.body
46
+ end
47
+
48
+ # Get checkout session from client (no auth required)
49
+ # @param id [String] Checkout session ID
50
+ # @return [Hash] Checkout session data
51
+ def client_get(id)
52
+ response = get("/checkouts/#{id}/client")
53
+ response.body
54
+ end
55
+
56
+ # Update checkout session from client
57
+ # @param id [String] Checkout session ID
58
+ # @param attributes [Hash] Updated attributes
59
+ # @return [Hash] Updated checkout session
60
+ def client_update(id, attributes)
61
+ response = patch("/checkouts/#{id}/client", attributes)
62
+ response.body
63
+ end
64
+
65
+ # Confirm checkout session from client
66
+ # @param id [String] Checkout session ID
67
+ # @param confirmation_data [Hash] Confirmation data
68
+ # @return [Hash] Confirmed checkout session
69
+ def client_confirm(id, confirmation_data = {})
70
+ response = post("/checkouts/#{id}/client/confirm", confirmation_data)
71
+ response.body
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Polar
6
+ module Resources
7
+ class Customers < Base
8
+ # List customers
9
+ # @param params [Hash] Query parameters
10
+ # @option params [String] :organization_id Filter by organization ID
11
+ # @option params [String] :query Search query
12
+ # @option params [Integer] :page Page number
13
+ # @option params [Integer] :limit Items per page
14
+ # @return [PaginatedResponse] Paginated list of customers
15
+ def list(params = {})
16
+ paginate('/customers/', params)
17
+ end
18
+
19
+ # Create a customer
20
+ # @param attributes [Hash] Customer attributes
21
+ # @option attributes [String] :email Customer email
22
+ # @option attributes [String] :name Customer name
23
+ # @option attributes [String] :organization_id Organization ID
24
+ # @return [Hash] Created customer
25
+ def create(attributes)
26
+ response = post('/customers/', attributes)
27
+ response.body
28
+ end
29
+
30
+ # Export customers
31
+ # @param params [Hash] Export parameters
32
+ # @return [Hash] Export job details
33
+ def export(params = {})
34
+ response = post('/customers/export', params)
35
+ response.body
36
+ end
37
+
38
+ # Get a customer by ID
39
+ # @param id [String] Customer ID
40
+ # @return [Hash] Customer data
41
+ def get(id)
42
+ response = get("/customers/#{id}")
43
+ response.body
44
+ end
45
+
46
+ # Update a customer
47
+ # @param id [String] Customer ID
48
+ # @param attributes [Hash] Updated attributes
49
+ # @return [Hash] Updated customer
50
+ def update(id, attributes)
51
+ response = patch("/customers/#{id}", attributes)
52
+ response.body
53
+ end
54
+
55
+ # Delete a customer
56
+ # @param id [String] Customer ID
57
+ # @return [Boolean] Success status
58
+ def delete(id)
59
+ delete("/customers/#{id}")
60
+ true
61
+ end
62
+
63
+ # Get customer by external ID
64
+ # @param external_id [String] External customer ID
65
+ # @param organization_id [String] Organization ID
66
+ # @return [Hash] Customer data
67
+ def get_external(external_id, organization_id:)
68
+ params = { organization_id: organization_id }
69
+ response = get("/customers/external/#{external_id}", params)
70
+ response.body
71
+ end
72
+
73
+ # Update customer by external ID
74
+ # @param external_id [String] External customer ID
75
+ # @param attributes [Hash] Updated attributes
76
+ # @option attributes [String] :organization_id Organization ID (required)
77
+ # @return [Hash] Updated customer
78
+ def update_external(external_id, attributes)
79
+ response = patch("/customers/external/#{external_id}", attributes)
80
+ response.body
81
+ end
82
+
83
+ # Delete customer by external ID
84
+ # @param external_id [String] External customer ID
85
+ # @param organization_id [String] Organization ID
86
+ # @return [Boolean] Success status
87
+ def delete_external(external_id, organization_id:)
88
+ params = { organization_id: organization_id }
89
+ delete("/customers/external/#{external_id}", params)
90
+ true
91
+ end
92
+
93
+ # Get customer state
94
+ # @param id [String] Customer ID
95
+ # @return [Hash] Customer state data
96
+ def get_state(id)
97
+ response = get("/customers/#{id}/state")
98
+ response.body
99
+ end
100
+
101
+ # Get customer state by external ID
102
+ # @param external_id [String] External customer ID
103
+ # @param organization_id [String] Organization ID
104
+ # @return [Hash] Customer state data
105
+ def get_state_external(external_id, organization_id:)
106
+ params = { organization_id: organization_id }
107
+ response = get("/customers/external/#{external_id}/state", params)
108
+ response.body
109
+ end
110
+
111
+ # Get customer balance
112
+ # @param id [String] Customer ID
113
+ # @return [Hash] Customer balance data
114
+ def get_balance(id)
115
+ response = get("/customers/#{id}/balance")
116
+ response.body
117
+ end
118
+ end
119
+ end
120
+ end