Project

xero-kiwi

0.0
The project is in a healthy, maintained state
Xero Kiwi handles the unglamorous parts of integrating with Xero — OAuth2 with PKCE, automatic token refresh, rate-limit-aware retries, and typed value objects for accounting resources — so your code can focus on the business problem rather than the plumbing.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.2
~> 2.0
>= 2.7, < 4.0
>= 5.0, < 7.0
 Project Readme

Xero Kiwi

A Ruby wrapper for the Xero Accounting API. Xero Kiwi handles the unglamorous parts of integrating with Xero — OAuth2, token refresh, rate limiting, retries — so the rest of your code can focus on the actual business problem.

What's in the box

  • Full OAuth2 authorization-code flow with PKCE support, CSRF state helpers, and OIDC ID token verification against Xero's JWKS.
  • Automatic token refresh with proactive (before expiry) and reactive (on-401) handling, a callback hook for persisting rotated tokens, and a mutex to dedupe concurrent refreshes from multiple threads.
  • Rate-limit-aware retries that honour Xero's Retry-After header on 429s and back off on transient 5xxs, built on faraday-retry.
  • A discoverable client surface with explicit error classes for every failure mode (authentication, rate limit, code exchange, ID token verification, CSRF mismatch).
  • Connection management: list and disconnect tenants, with token revocation for "disconnect Xero from my app" flows.
  • Accounting resources: fetch contacts, organisations, users, branding themes, and nested objects like addresses, phones, external links, and payment terms — all wrapped in proper value objects.
  • Querying: every list endpoint accepts where / order / page / page_size / modified_since, with a typed hash DSL (where: { status: "AUTHORISED" }) and raw-string escape hatches. Lazy each_<resource> helpers walk every page for whole-tenant scans and incremental syncs.
  • Resumable syncs: each_<resource>_page yields whole pages so you can record the page number alongside the rows, and start_page: picks the walk back up where it stopped.
  • Quota introspection: client.rate_limit(tenant_id) reports how much of a tenant's daily and per-minute allowance is left, taking the stricter of what Xero reported and what your own throttle bucket holds.

Installation

Add this line to your application's Gemfile:

gem "xero-kiwi"

Then run bundle install.

Xero Kiwi requires Ruby 3.4.1 or newer.

Quick start

require "xero_kiwi"

# Once you've completed the OAuth flow and have an access token:
client = XeroKiwi::Client.new(access_token: "ya29...")
client.connections.each do |connection|
  puts "#{connection.tenant_name} (#{connection.tenant_type})"
end

For the full OAuth flow, refresh handling, and everything else, see the docs below.

Documentation

Doc What it covers
Getting started Installation, the mental model, your first end-to-end request
Client XeroKiwi::Client — every constructor option, request lifecycle, configuration
Connections Listing tenants, the XeroKiwi::Connection resource, disconnecting tenants
Contacts Listing and fetching contacts, the XeroKiwi::Accounting::Contact resource, nested ContactPerson
Contact Groups Listing and fetching contact groups, the XeroKiwi::Accounting::ContactGroup resource
Organisation Fetching an organisation, the XeroKiwi::Accounting::Organisation resource, nested objects
Users Listing and fetching users, the XeroKiwi::Accounting::User resource, organisation roles
Credit Notes Listing and fetching credit notes, the XeroKiwi::Accounting::CreditNote resource
Invoices Listing and fetching invoices/bills, the XeroKiwi::Accounting::Invoice resource
Payments Listing and fetching payments, the XeroKiwi::Accounting::Payment resource
Overpayments Listing and fetching overpayments, the XeroKiwi::Accounting::Overpayment resource
Prepayments Listing and fetching prepayments, the XeroKiwi::Accounting::Prepayment resource, LineItem
Branding Themes Listing and fetching branding themes, the XeroKiwi::Accounting::BrandingTheme resource
Tracking Categories Listing and fetching tracking categories, the TrackingCategory / TrackingOption / Tracking resources
Address The XeroKiwi::Accounting::Address value object, nested on Organisation and Contact
Phone The XeroKiwi::Accounting::Phone value object, nested on Organisation and Contact
ExternalLink The XeroKiwi::Accounting::ExternalLink value object
PaymentTerms The XeroKiwi::Accounting::PaymentTerms and PaymentTerm value objects
Tokens The XeroKiwi::Token value object, automatic refresh, revocation, persistence callbacks
OAuth Authorization URL building, code exchange, PKCE, ID token verification, full Rails-style example
Errors The error hierarchy, what to catch and when
Retries and rate limits How Xero Kiwi handles 429s and transient failures, customising the retry policy
Throttling Redis-backed token bucket for proactive rate-limit coordination across multiple workers
Querying where / order / page / page_size / include_archived / modified_since on list endpoints, the Page return type, each_* and each_*_page helpers, and start_page: resumability

Status

Xero Kiwi is in early development. The API surface for the features documented above is stable, but expect new resource methods to be added over time. Breaking changes will be called out in the changelog.

Development

The gem runs natively via Bundler — nothing is containerised for everyday development. A docker-compose.yml exists for external services the specs need, currently just Redis (used by the throttle limiter's Lua-backed specs).

bundle install
docker compose up -d redis   # optional; only needed for :redis-tagged specs
bundle exec rspec

Without Redis running, specs tagged :redis are filtered out automatically (see spec/spec_helper.rb) so the suite still passes. Override the test Redis URL with TEST_REDIS_URL=... if you don't want the default redis://127.0.0.1:6379/15.

docker compose down          # when you're done

Checking a resource against real payloads

bundle exec rake xero:coverage

Compares every recorded response in spec/fixtures/vcr_cassettes against the resource classes that model it, and reports four things: keys Xero sends that nothing reads, attributes that were nil in every recording, declared types that disagree with what arrived, and classes no recording exercises at all.

Run it whenever you add or change a resource. Every silent bug this gem has shipped would have appeared in one of those four lists, and in each case the disproving payload was already committed — the recordings were never the gap, reading them was.

It reports rather than fails, and deliberately. Gating it would need an allowlist of the legitimately-absent keys, and an allowlist becomes a list nobody reads — which is the failure this is meant to prevent. Read the output instead.

bundle exec rake xero:schema

The other half: compares the resource classes against Xero's published OpenAPI spec, downloaded and cached for a day. A recording shows only what one tenant happened to populate; the spec lists what an endpoint can return at all.

Run both. Neither is sufficient, and each has caught what the other missed:

Found by
LineItem read AccountId, Xero sends AccountID spec — no recording here carries a line item
User#global_user_id recordings — absent from the spec entirely
CreditNote#due_date would be a gap neither — the spec documents it, Xero sends it on none

Treat a difference as a question rather than a defect. A payload settles it where one exists.

Releasing

Merge first, then bump. rake release tags whatever is checked out, so a version bump that rides along inside a pull request gets orphaned the moment that PR is squash-merged — the squash creates a new commit and the tag is left pointing at the pre-merge branch commit, unreachable from main. Bumping after the merge makes that impossible regardless of merge strategy.

So: land the work through a PR as normal, then

git checkout main && git pull
  1. Bump VERSION in lib/xero_kiwi/version.rb.
  2. Move the [Unreleased] entries in CHANGELOG.md under a dated version heading.
  3. bundle install to refresh the lockfile's xero-kiwi (x.y.z).
  4. bundle exec rake — specs, RuboCop, and the llms:check freshness guard.
  5. Commit to main. A commit-msg hook requires the crimesjohnson: prefix there, so: crimesjohnson: release x.y.z with <the headline>.
bundle exec rake release     # builds, tags, pushes the tag, publishes

The gemspec sets rubygems_mfa_required, so this prompts for an OTP and can't run unattended. Finally, cut the GitHub release from the changelog section:

gh release create vx.y.z --title "vx.y.z — <headline>" --notes-file <(…) --verify-tag --latest

Note that docs/ ships inside the gem, so a documentation-only fix still needs a patch release to reach anyone — it won't arrive through GitHub alone.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/douglasgreyling/xero-kiwi.

License

The gem is available as open source under the terms of the MIT License.