Class: XeroKiwi::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/xero_kiwi/client.rb

Overview

Entry point for talking to Xero. Holds the OAuth2 token state, knows how to refresh it (when given client credentials), and exposes resource methods that auto-refresh before each request.

# Simple — access token only, no refresh capability.
client = XeroKiwi::Client.new(access_token: "ya29...")

# Full — refresh-capable, with persistence callback.
client = XeroKiwi::Client.new(
  access_token:     creds.access_token,
  refresh_token:    creds.refresh_token,
  expires_at:       creds.expires_at,
  client_id:        ENV["XERO_CLIENT_ID"],
  client_secret:    ENV["XERO_CLIENT_SECRET"],
  on_token_refresh: ->(token) { creds.update!(token.to_h) }
)

client.token             # => XeroKiwi::Token
client.token.expired?    # => false
client.refresh_token!    # manual force refresh
client.connections       # auto-refreshes if expiring; reactive on 401

Defined Under Namespace

Classes: ResponseHandler

Constant Summary collapse

BASE_URL =
"https://api.xero.com"
DEFAULT_USER_AGENT =
"XeroKiwi/#{XeroKiwi::VERSION} (+https://github.com/douglasgreyling/xero-kiwi)".freeze
RETRY_STATUSES =

HTTP statuses we treat as transient. faraday-retry honours Retry-After automatically when the status is in this list.

[429, 502, 503, 504].freeze
DEFAULT_RETRY_OPTIONS =
{
  max:                 4,
  interval:            0.5,
  interval_randomness: 0.5,
  backoff_factor:      2,
  retry_statuses:      RETRY_STATUSES,
  methods:             %i[get head options put delete post],
  # Faraday::RetriableResponse is the *internal* signal faraday-retry uses
  # to flag a status-code retry. It MUST be in this list, or the middleware
  # can't catch its own retry signal and 429s/503s never get retried.
  exceptions:          [
    Faraday::ConnectionFailed,
    Faraday::TimeoutError,
    Faraday::RetriableResponse,
    Errno::ETIMEDOUT
  ]
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(access_token:, refresh_token: nil, expires_at: nil, client_id: nil, client_secret: nil, on_token_refresh: nil, adapter: nil, user_agent: DEFAULT_USER_AGENT, retry_options: {}, throttle: nil) ⇒ Client

Returns a new instance of Client.



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/xero_kiwi/client.rb', line 56

def initialize(
  access_token:,
  refresh_token: nil,
  expires_at: nil,
  client_id: nil,
  client_secret: nil,
  on_token_refresh: nil,
  adapter: nil,
  user_agent: DEFAULT_USER_AGENT,
  retry_options: {},
  throttle: nil
)
  @token            = Token.new(
    access_token:  access_token,
    refresh_token: refresh_token,
    expires_at:    expires_at
  )
  @client_id        = client_id
  @client_secret    = client_secret
  @on_token_refresh = on_token_refresh
  @adapter          = adapter
  @user_agent       = user_agent
  @retry_options    = DEFAULT_RETRY_OPTIONS.merge(retry_options)
  @throttle         = throttle || Throttle::NullLimiter.new
  @refresh_mutex    = Mutex.new
end

Instance Attribute Details

#tokenObject (readonly)

Returns the value of attribute token.



54
55
56
# File 'lib/xero_kiwi/client.rb', line 54

def token
  @token
end

Instance Method Details

#branding_theme(tenant_id, branding_theme_id) ⇒ Object

Fetches a single Branding Theme by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/brandingthemes

Raises:

  • (ArgumentError)


389
390
391
392
393
394
395
396
397
398
399
400
# File 'lib/xero_kiwi/client.rb', line 389

def branding_theme(tenant_id, branding_theme_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "branding_theme_id is required" if branding_theme_id.nil? || branding_theme_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/BrandingThemes/#{branding_theme_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::BrandingTheme.from_response(response.body).first
  end
end

#branding_themes(tenant_id) ⇒ Object

Fetches the Branding Themes for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/brandingthemes

Raises:

  • (ArgumentError)


374
375
376
377
378
379
380
381
382
383
384
# File 'lib/xero_kiwi/client.rb', line 374

def branding_themes(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/BrandingThemes") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::BrandingTheme.from_response(response.body)
  end
end

#can_refresh?Boolean

True if this client was constructed with refresh credentials AND the current token still carries a refresh_token to use.

Returns:

  • (Boolean)


439
440
441
# File 'lib/xero_kiwi/client.rb', line 439

def can_refresh?
  !@client_id.nil? && !@client_secret.nil? && @token.refreshable?
end

#connectionsObject

Fetches the list of tenants the current access token has access to. See: developer.xero.com/documentation/best-practices/managing-connections/connections



85
86
87
88
89
90
# File 'lib/xero_kiwi/client.rb', line 85

def connections
  with_authenticated_request do
    response = http.get("/connections")
    Connection.from_response(response.body)
  end
end

#contact(tenant_id, contact_id) ⇒ Object

Fetches a single Contact by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/contacts

Raises:

  • (ArgumentError)


156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/xero_kiwi/client.rb', line 156

def contact(tenant_id, contact_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "contact_id is required" if contact_id.nil? || contact_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Contacts/#{contact_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Contact.from_response(response.body).first
  end
end

#contact_group(tenant_id, contact_group_id) ⇒ Object

Fetches a single Contact Group by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/contactgroups

Raises:

  • (ArgumentError)


187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/xero_kiwi/client.rb', line 187

def contact_group(tenant_id, contact_group_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "contact_group_id is required" if contact_group_id.nil? || contact_group_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/ContactGroups/#{contact_group_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::ContactGroup.from_response(response.body).first
  end
end

#contact_groups(tenant_id) ⇒ Object

Fetches the Contact Groups for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/contactgroups

Raises:

  • (ArgumentError)


172
173
174
175
176
177
178
179
180
181
182
# File 'lib/xero_kiwi/client.rb', line 172

def contact_groups(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/ContactGroups") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::ContactGroup.from_response(response.body)
  end
end

#contacts(tenant_id) ⇒ Object

Fetches the Contacts for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/contacts

Raises:

  • (ArgumentError)


141
142
143
144
145
146
147
148
149
150
151
# File 'lib/xero_kiwi/client.rb', line 141

def contacts(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Contacts") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Contact.from_response(response.body)
  end
end

#credit_note(tenant_id, credit_note_id) ⇒ Object

Fetches a single Credit Note by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/creditnotes

Raises:

  • (ArgumentError)


249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/xero_kiwi/client.rb', line 249

def credit_note(tenant_id, credit_note_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "credit_note_id is required" if credit_note_id.nil? || credit_note_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/CreditNotes/#{credit_note_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::CreditNote.from_response(response.body).first
  end
end

#credit_notes(tenant_id) ⇒ Object

Fetches the Credit Notes for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/creditnotes

Raises:

  • (ArgumentError)


234
235
236
237
238
239
240
241
242
243
244
# File 'lib/xero_kiwi/client.rb', line 234

def credit_notes(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/CreditNotes") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::CreditNote.from_response(response.body)
  end
end

#delete_connection(connection_or_id) ⇒ Object

Disconnects a tenant. Accepts either a XeroKiwi::Connection (we use its ‘id`) or a raw connection-id string. Returns true on the 204. The access token may still be valid for other connections after this —only the named tenant is detached.

Raises:

  • (ArgumentError)


406
407
408
409
410
411
412
413
414
# File 'lib/xero_kiwi/client.rb', line 406

def delete_connection(connection_or_id)
  id = extract_connection_id(connection_or_id)
  raise ArgumentError, "connection id is required" if id.nil? || id.empty?

  with_authenticated_request do
    http.delete("/connections/#{id}")
    true
  end
end

#invoice(tenant_id, invoice_id) ⇒ Object

Fetches a single Invoice by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/invoices

Raises:

  • (ArgumentError)


342
343
344
345
346
347
348
349
350
351
352
353
# File 'lib/xero_kiwi/client.rb', line 342

def invoice(tenant_id, invoice_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "invoice_id is required" if invoice_id.nil? || invoice_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Invoices/#{invoice_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Invoice.from_response(response.body).first
  end
end

#invoices(tenant_id) ⇒ Object

Fetches the Invoices for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/invoices

Raises:

  • (ArgumentError)


327
328
329
330
331
332
333
334
335
336
337
# File 'lib/xero_kiwi/client.rb', line 327

def invoices(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Invoices") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Invoice.from_response(response.body)
  end
end

#online_invoice_url(tenant_id, invoice_id) ⇒ Object

Fetches the online invoice URL for a sales (ACCREC) invoice. Returns the URL string, or nil if not available. Cannot be used on DRAFT invoices. See: developer.xero.com/documentation/api/accounting/invoices

Raises:

  • (ArgumentError)


358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/xero_kiwi/client.rb', line 358

def online_invoice_url(tenant_id, invoice_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "invoice_id is required" if invoice_id.nil? || invoice_id.to_s.empty?

  data = with_authenticated_request do
    http.get("/api.xro/2.0/Invoices/#{invoice_id}/OnlineInvoice") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
  end
  data.body.dig("OnlineInvoices", 0, "OnlineInvoiceUrl")
end

#organisation(tenant_id) ⇒ Object

Fetches the Organisation for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/organisation

Raises:

  • (ArgumentError)


95
96
97
98
99
100
101
102
103
104
105
# File 'lib/xero_kiwi/client.rb', line 95

def organisation(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Organisation") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Organisation.from_response(response.body)
  end
end

#overpayment(tenant_id, overpayment_id) ⇒ Object

Fetches a single Overpayment by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/overpayments

Raises:

  • (ArgumentError)


280
281
282
283
284
285
286
287
288
289
290
291
# File 'lib/xero_kiwi/client.rb', line 280

def overpayment(tenant_id, overpayment_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "overpayment_id is required" if overpayment_id.nil? || overpayment_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Overpayments/#{overpayment_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Overpayment.from_response(response.body).first
  end
end

#overpayments(tenant_id) ⇒ Object

Fetches the Overpayments for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/overpayments

Raises:

  • (ArgumentError)


265
266
267
268
269
270
271
272
273
274
275
# File 'lib/xero_kiwi/client.rb', line 265

def overpayments(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Overpayments") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Overpayment.from_response(response.body)
  end
end

#payment(tenant_id, payment_id) ⇒ Object

Fetches a single Payment by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/payments

Raises:

  • (ArgumentError)


311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/xero_kiwi/client.rb', line 311

def payment(tenant_id, payment_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "payment_id is required" if payment_id.nil? || payment_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Payments/#{payment_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Payment.from_response(response.body).first
  end
end

#payments(tenant_id) ⇒ Object

Fetches the Payments for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/payments

Raises:

  • (ArgumentError)


296
297
298
299
300
301
302
303
304
305
306
# File 'lib/xero_kiwi/client.rb', line 296

def payments(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Payments") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Payment.from_response(response.body)
  end
end

#prepayment(tenant_id, prepayment_id) ⇒ Object

Fetches a single Prepayment by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/prepayments

Raises:

  • (ArgumentError)


218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/xero_kiwi/client.rb', line 218

def prepayment(tenant_id, prepayment_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "prepayment_id is required" if prepayment_id.nil? || prepayment_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Prepayments/#{prepayment_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Prepayment.from_response(response.body).first
  end
end

#prepayments(tenant_id) ⇒ Object

Fetches the Prepayments for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/prepayments

Raises:

  • (ArgumentError)


203
204
205
206
207
208
209
210
211
212
213
# File 'lib/xero_kiwi/client.rb', line 203

def prepayments(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Prepayments") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::Prepayment.from_response(response.body)
  end
end

#refresh_token!Object

Forces a refresh regardless of expiry. Returns the new Token. Raises TokenRefreshError if refresh credentials are missing or if Xero rejects the refresh.

Raises:



431
432
433
434
435
# File 'lib/xero_kiwi/client.rb', line 431

def refresh_token!
  raise TokenRefreshError.new(nil, nil, "client has no refresh capability") unless can_refresh?

  @refresh_mutex.synchronize { perform_refresh }
end

#revoke_token!Object

Revokes the current refresh token at Xero, invalidating it and every access token issued from it. Use this for “disconnect Xero” / logout flows. After this call, treat the client as dead — subsequent API calls will 401. The caller is responsible for cleaning up any persisted credential record.

Raises:



421
422
423
424
425
426
# File 'lib/xero_kiwi/client.rb', line 421

def revoke_token!
  raise TokenRefreshError.new(nil, nil, "client has no refresh capability") unless can_refresh?

  revoker.revoke_token(refresh_token: @token.refresh_token)
  true
end

#user(tenant_id, user_id) ⇒ Object

Fetches a single User by ID for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/users

Raises:

  • (ArgumentError)


125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/xero_kiwi/client.rb', line 125

def user(tenant_id, user_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?
  raise ArgumentError, "user_id is required" if user_id.nil? || user_id.to_s.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Users/#{user_id}") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::User.from_response(response.body).first
  end
end

#users(tenant_id) ⇒ Object

Fetches the Users for the given tenant. Accepts a tenant-id string or a XeroKiwi::Connection (we use its tenant_id). See: developer.xero.com/documentation/api/accounting/users

Raises:

  • (ArgumentError)


110
111
112
113
114
115
116
117
118
119
120
# File 'lib/xero_kiwi/client.rb', line 110

def users(tenant_id)
  tid = extract_tenant_id(tenant_id)
  raise ArgumentError, "tenant_id is required" if tid.nil? || tid.empty?

  with_authenticated_request do
    response = http.get("/api.xro/2.0/Users") do |req|
      req.headers["Xero-Tenant-Id"] = tid
    end
    Accounting::User.from_response(response.body)
  end
end