xero-kiwi 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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +2 -0
  3. data/CHANGELOG.md +5 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +89 -0
  6. data/Rakefile +89 -0
  7. data/docs/accounting/address.md +54 -0
  8. data/docs/accounting/branding-theme.md +92 -0
  9. data/docs/accounting/contact-group.md +91 -0
  10. data/docs/accounting/contact.md +166 -0
  11. data/docs/accounting/credit-note.md +97 -0
  12. data/docs/accounting/external-link.md +33 -0
  13. data/docs/accounting/invoice.md +134 -0
  14. data/docs/accounting/organisation.md +119 -0
  15. data/docs/accounting/overpayment.md +94 -0
  16. data/docs/accounting/payment-terms.md +58 -0
  17. data/docs/accounting/payment.md +99 -0
  18. data/docs/accounting/phone.md +45 -0
  19. data/docs/accounting/prepayment.md +111 -0
  20. data/docs/accounting/user.md +109 -0
  21. data/docs/client.md +174 -0
  22. data/docs/connections.md +166 -0
  23. data/docs/errors.md +271 -0
  24. data/docs/getting-started.md +138 -0
  25. data/docs/oauth.md +508 -0
  26. data/docs/retries-and-rate-limits.md +224 -0
  27. data/docs/tokens.md +339 -0
  28. data/lib/xero_kiwi/accounting/address.rb +58 -0
  29. data/lib/xero_kiwi/accounting/allocation.rb +66 -0
  30. data/lib/xero_kiwi/accounting/branding_theme.rb +76 -0
  31. data/lib/xero_kiwi/accounting/contact.rb +153 -0
  32. data/lib/xero_kiwi/accounting/contact_group.rb +57 -0
  33. data/lib/xero_kiwi/accounting/contact_person.rb +45 -0
  34. data/lib/xero_kiwi/accounting/credit_note.rb +115 -0
  35. data/lib/xero_kiwi/accounting/external_link.rb +38 -0
  36. data/lib/xero_kiwi/accounting/invoice.rb +142 -0
  37. data/lib/xero_kiwi/accounting/line_item.rb +64 -0
  38. data/lib/xero_kiwi/accounting/organisation.rb +138 -0
  39. data/lib/xero_kiwi/accounting/overpayment.rb +107 -0
  40. data/lib/xero_kiwi/accounting/payment.rb +105 -0
  41. data/lib/xero_kiwi/accounting/payment_terms.rb +77 -0
  42. data/lib/xero_kiwi/accounting/phone.rb +46 -0
  43. data/lib/xero_kiwi/accounting/prepayment.rb +109 -0
  44. data/lib/xero_kiwi/accounting/tracking_category.rb +42 -0
  45. data/lib/xero_kiwi/accounting/user.rb +80 -0
  46. data/lib/xero_kiwi/client.rb +576 -0
  47. data/lib/xero_kiwi/connection.rb +78 -0
  48. data/lib/xero_kiwi/errors.rb +34 -0
  49. data/lib/xero_kiwi/identity.rb +40 -0
  50. data/lib/xero_kiwi/oauth/id_token.rb +102 -0
  51. data/lib/xero_kiwi/oauth/pkce.rb +51 -0
  52. data/lib/xero_kiwi/oauth.rb +232 -0
  53. data/lib/xero_kiwi/token.rb +99 -0
  54. data/lib/xero_kiwi/token_refresher.rb +53 -0
  55. data/lib/xero_kiwi/version.rb +5 -0
  56. data/lib/xero_kiwi.rb +33 -0
  57. data/llms-full.txt +3351 -0
  58. data/llms.txt +56 -0
  59. data/sig/xero_kiwi.rbs +4 -0
  60. metadata +164 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 8461f3c23bcec591acd4a6bb875ff08f4bacf67573ae6e1957ad78a268d4c7bb
4
+ data.tar.gz: 8167dae0e2922d57099b177a3ee182f3728c28dfc45f6fd0a50ae61a50ea0feb
5
+ SHA512:
6
+ metadata.gz: 1a55d9c5c9022a6fbf7fcc777d8084775d54e464d87008dfa876f234f792f5ec45a427ea55dfef0adb01ee8f1101ef49999206d64e6b8cec63f0a97fec3564dd
7
+ data.tar.gz: 95b28e21648463606521aff37c8413e7f6a73fa9f731d30098ef757cb480beaa2f1c3da3f588b1b00ca8cf00784dcd5ec9ca2c7853d129b35ed82ddf6bececaa
data/.env.example ADDED
@@ -0,0 +1,2 @@
1
+ XERO_ACCESS_TOKEN=xxx
2
+ XERO_TENANT_ID=xxx
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-04-15
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Douglas Greyling
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 ADDED
@@ -0,0 +1,89 @@
1
+ # Xero Kiwi
2
+
3
+ A Ruby wrapper for the [Xero](https://www.xero.com) Accounting API. XeroKiwi handles
4
+ the unglamorous parts of integrating with Xero — OAuth2, token refresh, rate
5
+ limiting, retries — so the rest of your code can focus on the actual business
6
+ problem.
7
+
8
+ ## What's in the box
9
+
10
+ - **Full OAuth2 authorization-code flow** with PKCE support, CSRF state helpers,
11
+ and OIDC ID token verification against Xero's JWKS.
12
+ - **Automatic token refresh** with proactive (before expiry) and reactive
13
+ (on-401) handling, a callback hook for persisting rotated tokens, and a
14
+ mutex to dedupe concurrent refreshes from multiple threads.
15
+ - **Rate-limit-aware retries** that honour Xero's `Retry-After` header on 429s
16
+ and back off on transient 5xxs, built on `faraday-retry`.
17
+ - **A discoverable client surface** with explicit error classes for every
18
+ failure mode (authentication, rate limit, code exchange, ID token verification,
19
+ CSRF mismatch).
20
+ - **Connection management**: list and disconnect tenants, with token revocation
21
+ for "disconnect Xero from my app" flows.
22
+ - **Accounting resources**: fetch contacts, organisations, users, branding
23
+ themes, and nested objects like addresses, phones, external links, and payment
24
+ terms — all wrapped in proper value objects.
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem "xero-kiwi"
32
+ ```
33
+
34
+ Then run `bundle install`.
35
+
36
+ XeroKiwi requires Ruby 3.4.1 or newer.
37
+
38
+ ## Quick start
39
+
40
+ ```ruby
41
+ require "xero_kiwi"
42
+
43
+ # Once you've completed the OAuth flow and have an access token:
44
+ client = XeroKiwi::Client.new(access_token: "ya29...")
45
+ client.connections.each do |connection|
46
+ puts "#{connection.tenant_name} (#{connection.tenant_type})"
47
+ end
48
+ ```
49
+
50
+ For the full OAuth flow, refresh handling, and everything else, see the docs
51
+ below.
52
+
53
+ ## Documentation
54
+
55
+ | Doc | What it covers |
56
+ |-----|----------------|
57
+ | [Getting started](docs/getting-started.md) | Installation, the mental model, your first end-to-end request |
58
+ | [Client](docs/client.md) | `XeroKiwi::Client` — every constructor option, request lifecycle, configuration |
59
+ | [Connections](docs/connections.md) | Listing tenants, the `XeroKiwi::Connection` resource, disconnecting tenants |
60
+ | [Contacts](docs/accounting/contact.md) | Listing and fetching contacts, the `XeroKiwi::Accounting::Contact` resource, nested ContactPerson |
61
+ | [Contact Groups](docs/accounting/contact-group.md) | Listing and fetching contact groups, the `XeroKiwi::Accounting::ContactGroup` resource |
62
+ | [Organisation](docs/accounting/organisation.md) | Fetching an organisation, the `XeroKiwi::Accounting::Organisation` resource, nested objects |
63
+ | [Users](docs/accounting/user.md) | Listing and fetching users, the `XeroKiwi::Accounting::User` resource, organisation roles |
64
+ | [Credit Notes](docs/accounting/credit-note.md) | Listing and fetching credit notes, the `XeroKiwi::Accounting::CreditNote` resource |
65
+ | [Invoices](docs/accounting/invoice.md) | Listing and fetching invoices/bills, the `XeroKiwi::Accounting::Invoice` resource |
66
+ | [Payments](docs/accounting/payment.md) | Listing and fetching payments, the `XeroKiwi::Accounting::Payment` resource |
67
+ | [Overpayments](docs/accounting/overpayment.md) | Listing and fetching overpayments, the `XeroKiwi::Accounting::Overpayment` resource |
68
+ | [Prepayments](docs/accounting/prepayment.md) | Listing and fetching prepayments, the `XeroKiwi::Accounting::Prepayment` resource, LineItem |
69
+ | [Branding Themes](docs/accounting/branding-theme.md) | Listing and fetching branding themes, the `XeroKiwi::Accounting::BrandingTheme` resource |
70
+ | [Tokens](docs/tokens.md) | The `XeroKiwi::Token` value object, automatic refresh, revocation, persistence callbacks |
71
+ | [OAuth](docs/oauth.md) | Authorization URL building, code exchange, PKCE, ID token verification, full Rails-style example |
72
+ | [Errors](docs/errors.md) | The error hierarchy, what to catch and when |
73
+ | [Retries and rate limits](docs/retries-and-rate-limits.md) | How XeroKiwi handles 429s and transient failures, customising the retry policy |
74
+
75
+ ## Status
76
+
77
+ XeroKiwi is in early development. The API surface for the features documented above
78
+ is stable, but expect new resource methods to be added over time. Breaking
79
+ changes will be called out in the [changelog](CHANGELOG.md).
80
+
81
+ ## Contributing
82
+
83
+ Bug reports and pull requests are welcome on GitHub at
84
+ https://github.com/douglasgreyling/xero-kiwi.
85
+
86
+ ## License
87
+
88
+ The gem is available as open source under the terms of the
89
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ # Files included in llms-full.txt, in reading order. Keep README first, then
13
+ # the docs in the same order the README's table of contents lists them.
14
+ LLMS_SOURCE_FILES = %w[
15
+ README.md
16
+ docs/getting-started.md
17
+ docs/client.md
18
+ docs/oauth.md
19
+ docs/tokens.md
20
+ docs/connections.md
21
+ docs/accounting/contact.md
22
+ docs/accounting/contact-group.md
23
+ docs/accounting/organisation.md
24
+ docs/accounting/user.md
25
+ docs/accounting/credit-note.md
26
+ docs/accounting/invoice.md
27
+ docs/accounting/payment.md
28
+ docs/accounting/overpayment.md
29
+ docs/accounting/prepayment.md
30
+ docs/accounting/branding-theme.md
31
+ docs/accounting/address.md
32
+ docs/accounting/phone.md
33
+ docs/accounting/external-link.md
34
+ docs/accounting/payment-terms.md
35
+ docs/errors.md
36
+ docs/retries-and-rate-limits.md
37
+ ].freeze
38
+
39
+ LLMS_FULL_PATH = "llms-full.txt"
40
+
41
+ # Builds the llms-full.txt content from LLMS_SOURCE_FILES. Pure function:
42
+ # returns a String, doesn't touch the filesystem. Both `llms:build` and
43
+ # `llms:check` use this so the two tasks can never disagree about the
44
+ # expected output.
45
+ def build_llms_full
46
+ out = +""
47
+ out << "# XeroKiwi — full documentation\n\n"
48
+ out << "This file is the complete documentation for the XeroKiwi gem (a Ruby wrapper for the Xero Accounting API), assembled into a single document for LLM consumption. It contains the README and every doc in the docs/ folder, in reading order.\n\n"
49
+ out << "For the curated index version, see llms.txt in the same directory.\n\n"
50
+ out << "Source: https://github.com/douglasgreyling/xero-kiwi\n\n"
51
+ LLMS_SOURCE_FILES.each { |path| append_file_block(out, path) }
52
+ out
53
+ end
54
+
55
+ def append_file_block(out, path)
56
+ separator = "=" * 80
57
+ out << "\n" << separator << "\n"
58
+ out << "FILE: #{path}\n"
59
+ out << separator << "\n\n"
60
+ out << File.read(path) << "\n"
61
+ end
62
+
63
+ namespace :llms do
64
+ desc "Regenerate llms-full.txt from README and docs/"
65
+ task :build do
66
+ File.write(LLMS_FULL_PATH, build_llms_full)
67
+ puts "Wrote #{LLMS_FULL_PATH} (#{File.size(LLMS_FULL_PATH)} bytes, #{LLMS_SOURCE_FILES.size} source files)"
68
+ end
69
+
70
+ desc "Verify llms-full.txt is up to date with README and docs/"
71
+ task :check do
72
+ expected = build_llms_full
73
+ actual = File.exist?(LLMS_FULL_PATH) ? File.read(LLMS_FULL_PATH) : ""
74
+
75
+ if expected == actual
76
+ puts "✓ #{LLMS_FULL_PATH} is up to date"
77
+ else
78
+ warn "✗ #{LLMS_FULL_PATH} is out of date with README/docs."
79
+ warn " Run `bundle exec rake llms:build` and commit the result."
80
+ exit 1
81
+ end
82
+ end
83
+ end
84
+
85
+ # Top-level convenience alias.
86
+ desc "Regenerate llms-full.txt (alias for llms:build)"
87
+ task llms: "llms:build"
88
+
89
+ task default: %i[spec rubocop llms:check]
@@ -0,0 +1,54 @@
1
+ # XeroKiwi::Accounting::Address
2
+
3
+ A Xero address. Used by [Organisation](organisation.md), and in future by
4
+ Contact and other resources.
5
+
6
+ > See: [Xero docs — Address types](https://developer.xero.com/documentation/api/accounting/types#addresses)
7
+
8
+ ## Usage
9
+
10
+ ```ruby
11
+ org.addresses.each do |address|
12
+ puts "#{address.address_type}: #{address.address_line_1}, #{address.city}"
13
+ end
14
+
15
+ street = org.addresses.find(&:street?)
16
+ ```
17
+
18
+ ## Attributes
19
+
20
+ | Attribute | Type | Notes |
21
+ |-----------|------|-------|
22
+ | `address_type` | `String` | `"POBOX"`, `"STREET"`, or `"DELIVERY"` |
23
+ | `address_line_1` | `String` | Max 500 characters |
24
+ | `address_line_2` | `String` | Max 500 characters |
25
+ | `address_line_3` | `String` | Max 500 characters |
26
+ | `address_line_4` | `String` | Max 500 characters |
27
+ | `city` | `String` | Max 255 characters |
28
+ | `region` | `String` | Max 255 characters |
29
+ | `postal_code` | `String` | Max 50 characters |
30
+ | `country` | `String` | Max 50 characters, letters only |
31
+ | `attention_to` | `String` | Max 255 characters |
32
+
33
+ ## Predicates
34
+
35
+ ```ruby
36
+ address.street? # address_type == "STREET"
37
+ address.pobox? # address_type == "POBOX"
38
+ address.delivery? # address_type == "DELIVERY"
39
+ ```
40
+
41
+ Note: `DELIVERY` is read-only via Xero's GET endpoint (if set) and is not
42
+ valid for Contacts — only for Organisations.
43
+
44
+ ## Equality
45
+
46
+ Two addresses are `==` if all their attributes match. `#hash` is consistent
47
+ with `==`.
48
+
49
+ ## Serialisation
50
+
51
+ ```ruby
52
+ address.to_h
53
+ # => { address_type: "STREET", address_line_1: "123 Main St", ... }
54
+ ```
@@ -0,0 +1,92 @@
1
+ # Branding Themes
2
+
3
+ A Xero **branding theme** controls the look and feel of invoices, quotes, and
4
+ other documents — logo, colours, layout. Every organisation has at least one
5
+ (the default "Standard" theme). You need a `tenant_id` from a
6
+ [connection](../connections.md) before you can fetch branding themes.
7
+
8
+ > See: [Xero docs — Branding Themes](https://developer.xero.com/documentation/api/accounting/brandingthemes)
9
+
10
+ ## Listing branding themes
11
+
12
+ ```ruby
13
+ client = XeroKiwi::Client.new(access_token: "ya29...")
14
+
15
+ # Pass a tenant ID string…
16
+ themes = client.branding_themes("70784a63-d24b-46a9-a4db-0e70a274b056")
17
+
18
+ # …or a XeroKiwi::Connection (its tenant_id is used automatically).
19
+ connection = client.connections.first
20
+ themes = client.branding_themes(connection)
21
+ ```
22
+
23
+ `client.branding_themes` hits `GET /api.xro/2.0/BrandingThemes` with the
24
+ `Xero-Tenant-Id` header set to the tenant you specify. It returns an
25
+ `Array<XeroKiwi::Accounting::BrandingTheme>`.
26
+
27
+ ## Fetching a single branding theme
28
+
29
+ ```ruby
30
+ theme = client.branding_theme(tenant_id, "dfe23d27-a3a6-4ef3-a5ca-b9e02b142dde")
31
+ theme.name # => "Special Projects"
32
+ ```
33
+
34
+ `client.branding_theme` hits `GET /api.xro/2.0/BrandingThemes/{BrandingThemeID}`
35
+ and returns a single `XeroKiwi::Accounting::BrandingTheme`, or `nil` if the
36
+ response is empty.
37
+
38
+ ## The BrandingTheme object
39
+
40
+ Each `XeroKiwi::Accounting::BrandingTheme` is an immutable value object exposing the
41
+ fields Xero returns:
42
+
43
+ | Attribute | Type | What it is |
44
+ |-----------|------|------------|
45
+ | `branding_theme_id` | `String` | The unique Xero identifier for the branding theme. |
46
+ | `name` | `String` | The display name (e.g. "Standard", "Special Projects"). |
47
+ | `logo_url` | `String` | The URL of the logo image used on the theme. May be `nil` if no custom logo is set. |
48
+ | `type` | `String` | The document type the theme applies to (always `"INVOICE"`). |
49
+ | `sort_order` | `Integer` | Ranked order of the theme. The default theme has a value of `0`. |
50
+ | `created_date_utc` | `Time` | When the theme was created, parsed as UTC. |
51
+
52
+ ## Equality and hashing
53
+
54
+ Two branding themes are `==` if they share the same `branding_theme_id`.
55
+ `#hash` is consistent with `==`, so branding themes work as hash keys and in
56
+ sets.
57
+
58
+ ## Date parsing
59
+
60
+ The `created_date_utc` field uses Xero's .NET JSON timestamp format
61
+ (`/Date(946684800000+0000)/`). XeroKiwi parses both .NET JSON and ISO 8601
62
+ formats transparently — the attribute is always a UTC `Time` object.
63
+
64
+ ## Error behaviour
65
+
66
+ | HTTP status | Exception | What it usually means |
67
+ |-------------|-----------|------------------------|
68
+ | 200 | (none — returns themes) | Success |
69
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
70
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
71
+ | 404 | `XeroKiwi::ClientError` | The branding theme ID doesn't exist in this organisation |
72
+
73
+ ## Common patterns
74
+
75
+ ### Listing all themes for a tenant
76
+
77
+ ```ruby
78
+ client = XeroKiwi::Client.new(access_token: "ya29...")
79
+
80
+ themes = client.branding_themes(tenant_id)
81
+ themes.each do |theme|
82
+ puts "#{theme.name} (sort: #{theme.sort_order})"
83
+ end
84
+ ```
85
+
86
+ ### Finding the default theme
87
+
88
+ ```ruby
89
+ themes = client.branding_themes(tenant_id)
90
+ default = themes.find { |t| t.sort_order == 0 }
91
+ puts "Default theme: #{default.name}"
92
+ ```
@@ -0,0 +1,91 @@
1
+ # Contact Groups
2
+
3
+ A Xero **contact group** is a named collection of contacts — useful for
4
+ organising customers or suppliers into categories like "VIP Customers" or
5
+ "Preferred Suppliers". You need a `tenant_id` from a
6
+ [connection](../connections.md) before you can fetch contact groups.
7
+
8
+ > See: [Xero docs — Contact Groups](https://developer.xero.com/documentation/api/accounting/contactgroups)
9
+
10
+ ## Listing contact groups
11
+
12
+ ```ruby
13
+ client = XeroKiwi::Client.new(access_token: "ya29...")
14
+
15
+ # Pass a tenant ID string…
16
+ groups = client.contact_groups("70784a63-d24b-46a9-a4db-0e70a274b056")
17
+
18
+ # …or a XeroKiwi::Connection (its tenant_id is used automatically).
19
+ connection = client.connections.first
20
+ groups = client.contact_groups(connection)
21
+ ```
22
+
23
+ `client.contact_groups` hits `GET /api.xro/2.0/ContactGroups` with the
24
+ `Xero-Tenant-Id` header set to the tenant you specify. It returns an
25
+ `Array<XeroKiwi::Accounting::ContactGroup>`. Only groups with status `ACTIVE` are
26
+ returned by Xero.
27
+
28
+ ## Fetching a single contact group
29
+
30
+ ```ruby
31
+ group = client.contact_group(tenant_id, "97bbd0e6-ab4d-4117-9304-d90dd4779199")
32
+ group.name # => "VIP Customers"
33
+ group.contacts # => [{"ContactID" => "...", "Name" => "Boom FM"}, ...]
34
+ ```
35
+
36
+ `client.contact_group` hits `GET /api.xro/2.0/ContactGroups/{ContactGroupID}`
37
+ and returns a single `XeroKiwi::Accounting::ContactGroup`, or `nil` if the
38
+ response is empty.
39
+
40
+ The single-group response includes a `contacts` array with the `ContactID` and
41
+ `Name` of each contact in the group. The list response does not include this.
42
+
43
+ ## The ContactGroup object
44
+
45
+ Each `XeroKiwi::Accounting::ContactGroup` is an immutable value object exposing the
46
+ fields Xero returns:
47
+
48
+ | Attribute | Type | What it is |
49
+ |-----------|------|------------|
50
+ | `contact_group_id` | `String` | The unique Xero identifier for the group. |
51
+ | `name` | `String` | The display name (e.g. "VIP Customers"). |
52
+ | `status` | `String` | `"ACTIVE"` (only active groups are returned by Xero). |
53
+ | `contacts` | `Array<XeroKiwi::Accounting::Contact>` | The contacts in the group (references — each has `reference?` returning `true`). Only present when fetching a single group. See [Contacts](contact.md). |
54
+
55
+ ## Predicates
56
+
57
+ ```ruby
58
+ group.active? # status == "ACTIVE"
59
+ ```
60
+
61
+ ## Equality and hashing
62
+
63
+ Two contact groups are `==` if they share the same `contact_group_id`. `#hash`
64
+ is consistent with `==`, so contact groups work as hash keys and in sets.
65
+
66
+ ## Error behaviour
67
+
68
+ | HTTP status | Exception | What it usually means |
69
+ |-------------|-----------|------------------------|
70
+ | 200 | (none — returns groups) | Success |
71
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
72
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
73
+ | 404 | `XeroKiwi::ClientError` | The contact group ID doesn't exist |
74
+
75
+ ## Common patterns
76
+
77
+ ### Listing all contact groups
78
+
79
+ ```ruby
80
+ groups = client.contact_groups(tenant_id)
81
+ groups.each { |g| puts g.name }
82
+ ```
83
+
84
+ ### Fetching members of a group
85
+
86
+ ```ruby
87
+ group = client.contact_group(tenant_id, "97bbd0e6-ab4d-4117-9304-d90dd4779199")
88
+ group.contacts.each do |c|
89
+ puts "#{c.name} (#{c.contact_id})"
90
+ end
91
+ ```
@@ -0,0 +1,166 @@
1
+ # Contacts
2
+
3
+ A Xero **contact** is a person or organisation you do business with — customers,
4
+ suppliers, or both. Contacts carry addresses, phone numbers, payment terms, and
5
+ metadata like tax type defaults. You need a `tenant_id` from a
6
+ [connection](../connections.md) before you can fetch contacts.
7
+
8
+ > See: [Xero docs — Contacts](https://developer.xero.com/documentation/api/accounting/contacts)
9
+
10
+ ## Listing contacts
11
+
12
+ ```ruby
13
+ client = XeroKiwi::Client.new(access_token: "ya29...")
14
+
15
+ # Pass a tenant ID string…
16
+ contacts = client.contacts("70784a63-d24b-46a9-a4db-0e70a274b056")
17
+
18
+ # …or a XeroKiwi::Connection (its tenant_id is used automatically).
19
+ connection = client.connections.first
20
+ contacts = client.contacts(connection)
21
+ ```
22
+
23
+ `client.contacts` hits `GET /api.xro/2.0/Contacts` with the `Xero-Tenant-Id`
24
+ header set to the tenant you specify. It returns an
25
+ `Array<XeroKiwi::Accounting::Contact>`.
26
+
27
+ ## Fetching a single contact
28
+
29
+ ```ruby
30
+ contact = client.contact(tenant_id, "bd2270c3-8706-4c11-9cfb-000b551c3f51")
31
+ contact.name # => "ABC Limited"
32
+ ```
33
+
34
+ `client.contact` hits `GET /api.xro/2.0/Contacts/{ContactID}` and returns a
35
+ single `XeroKiwi::Accounting::Contact`, or `nil` if the response is empty.
36
+
37
+ Fetching a single contact returns additional fields that are not included in the
38
+ list response (e.g. `contact_persons`, `payment_terms`, `website`).
39
+
40
+ ## The Contact object
41
+
42
+ Each `XeroKiwi::Accounting::Contact` is an immutable value object. The fields below
43
+ are always returned on list and single-contact responses:
44
+
45
+ | Attribute | Type | What it is |
46
+ |-----------|------|------------|
47
+ | `contact_id` | `String` | The unique Xero identifier for the contact. |
48
+ | `contact_number` | `String` | External system identifier (read-only in Xero UI). |
49
+ | `account_number` | `String` | A user-defined account number. |
50
+ | `contact_status` | `String` | `"ACTIVE"` or `"ARCHIVED"`. |
51
+ | `name` | `String` | Full name of the contact or organisation. |
52
+ | `first_name` | `String` | First name of the contact person. |
53
+ | `last_name` | `String` | Last name of the contact person. |
54
+ | `email_address` | `String` | Email address of the contact person. |
55
+ | `bank_account_details` | `String` | Bank account number. |
56
+ | `company_number` | `String` | Company registration number (max 50 chars). |
57
+ | `tax_number` | `String` | ABN / GST / VAT / Tax ID Number. |
58
+ | `tax_number_type` | `String` | Regional type of tax number (e.g. `"ABN"`). |
59
+ | `accounts_receivable_tax_type` | `String` | Default AR invoice tax type. |
60
+ | `accounts_payable_tax_type` | `String` | Default AP invoice tax type. |
61
+ | `addresses` | `Array<XeroKiwi::Accounting::Address>` | The contact's addresses. See [Address](address.md). |
62
+ | `phones` | `Array<XeroKiwi::Accounting::Phone>` | The contact's phone numbers. See [Phone](phone.md). |
63
+ | `is_supplier` | `Boolean` | Whether the contact has any AP invoices. |
64
+ | `is_customer` | `Boolean` | Whether the contact has any AR invoices. |
65
+ | `default_currency` | `String` | Default currency code (e.g. `"NZD"`). |
66
+ | `updated_date_utc` | `Time` | When the contact was last modified, parsed as UTC. |
67
+
68
+ ### Additional fields (single contact / paginated responses only)
69
+
70
+ | Attribute | Type | What it is |
71
+ |-----------|------|------------|
72
+ | `contact_persons` | `Array<XeroKiwi::Accounting::ContactPerson>` | Up to 5 contact people. See [ContactPerson](#the-contactperson-object). |
73
+ | `xero_network_key` | `String` | Xero network key for the contact. |
74
+ | `merged_to_contact_id` | `String` | ID of the destination contact if merged. |
75
+ | `sales_default_account_code` | `String` | Default sales account code. |
76
+ | `purchases_default_account_code` | `String` | Default purchases account code. |
77
+ | `sales_tracking_categories` | `Array<Hash>` | Default sales tracking categories (raw). |
78
+ | `purchases_tracking_categories` | `Array<Hash>` | Default purchases tracking categories (raw). |
79
+ | `sales_default_line_amount_type` | `String` | `"INCLUSIVE"`, `"EXCLUSIVE"`, or `"NONE"`. |
80
+ | `purchases_default_line_amount_type` | `String` | `"INCLUSIVE"`, `"EXCLUSIVE"`, or `"NONE"`. |
81
+ | `tracking_category_name` | `String` | Tracking category name. |
82
+ | `tracking_option_name` | `String` | Tracking option name. |
83
+ | `payment_terms` | `XeroKiwi::Accounting::PaymentTerms` | Default payment terms. See [PaymentTerms](payment-terms.md). |
84
+ | `contact_groups` | `Array<Hash>` | Contact groups the contact belongs to (raw). |
85
+ | `website` | `String` | Website URL. |
86
+ | `branding_theme` | `Hash` | Default branding theme (raw). |
87
+ | `batch_payments` | `Hash` | Batch payment details (raw). |
88
+ | `discount` | `Float` | Default discount rate. |
89
+ | `balances` | `Hash` | Outstanding and overdue AR/AP balances (raw). |
90
+ | `has_attachments` | `Boolean` | Whether the contact has attachments. |
91
+
92
+ ## The ContactPerson object
93
+
94
+ Each `XeroKiwi::Accounting::ContactPerson` is an immutable value object:
95
+
96
+ | Attribute | Type | What it is |
97
+ |-----------|------|------------|
98
+ | `first_name` | `String` | First name. |
99
+ | `last_name` | `String` | Last name. |
100
+ | `email_address` | `String` | Email address. |
101
+ | `include_in_emails` | `Boolean` | Whether to include on invoice emails. |
102
+
103
+ With a predicate: `contact_person.include_in_emails?`
104
+
105
+ ## Predicates
106
+
107
+ ```ruby
108
+ contact.reference? # true if this is a lightweight reference from another resource
109
+ contact.supplier? # is_supplier == true
110
+ contact.customer? # is_customer == true
111
+ contact.active? # contact_status == "ACTIVE"
112
+ contact.archived? # contact_status == "ARCHIVED"
113
+ ```
114
+
115
+ ### Contact references
116
+
117
+ When a contact appears nested inside another resource (e.g. an Invoice, Prepayment,
118
+ or CreditNote), it is wrapped as a `XeroKiwi::Accounting::Contact` with `reference?`
119
+ returning `true`. Reference contacts typically carry only a subset of fields (like
120
+ `contact_id` and `name`) — other fields will be `nil`.
121
+
122
+ ```ruby
123
+ invoice = client.invoice(tenant_id, invoice_id)
124
+ invoice.contact.name # => "City Agency"
125
+ invoice.contact.reference? # => true
126
+
127
+ # Fetch the full contact if you need all fields:
128
+ full_contact = client.contact(tenant_id, invoice.contact.contact_id)
129
+ full_contact.reference? # => false
130
+ full_contact.email_address # => "a.dutchess@abclimited.com"
131
+ ```
132
+
133
+ ## Equality and hashing
134
+
135
+ Two contacts are `==` if they share the same `contact_id`. `#hash` is consistent
136
+ with `==`, so contacts work as hash keys and in sets.
137
+
138
+ ## Error behaviour
139
+
140
+ | HTTP status | Exception | What it usually means |
141
+ |-------------|-----------|------------------------|
142
+ | 200 | (none — returns contacts) | Success |
143
+ | 401 | `XeroKiwi::AuthenticationError` | Access token is invalid or expired |
144
+ | 403 | `XeroKiwi::ClientError` | The token doesn't have the required scope |
145
+ | 404 | `XeroKiwi::ClientError` | The contact ID doesn't exist in this organisation |
146
+
147
+ ## Common patterns
148
+
149
+ ### Listing all customers
150
+
151
+ ```ruby
152
+ contacts = client.contacts(tenant_id)
153
+ customers = contacts.select(&:customer?)
154
+ customers.each { |c| puts "#{c.name} (#{c.default_currency})" }
155
+ ```
156
+
157
+ ### Fetching a contact with full details
158
+
159
+ ```ruby
160
+ # The list response omits some fields. Fetch individually for full details.
161
+ contact = client.contact(tenant_id, "bd2270c3-8706-4c11-9cfb-000b551c3f51")
162
+ puts contact.website
163
+ contact.contact_persons.each do |person|
164
+ puts " #{person.first_name} #{person.last_name} — #{person.email_address}"
165
+ end
166
+ ```