| name | xero-cli |
| description | Use when reading Xero accounting data from the command line or scripts — listing/looking up contacts, invoices, payments, bank transactions, manual journals; running reports (Trial Balance, Balance Sheet, P&L, BankSummary, Aged Receivables); downloading invoice PDFs or attachments; or managing the Xero OAuth2 connection (authorize, refresh, and verify you are connected to the correct organisation, not a trial/wrong tenant). Generic over any Xero org. |
xero-cli
A small, dependency-light swiss-army CLI + Python client over the Xero accounting
API, built for catch-up bookkeeping and audit reconstruction — bulk reads,
range-filtered extraction, report pulls, PDF/attachment download — not for live
transactional posting.
Core principle: the most expensive Xero auth failure is silently authorizing
against the wrong organisation (a trial org, a different company file). This tool
makes "which org am I actually talking to?" a first-class, enforced check.
When to use
- "Pull FY18-19 bank transactions for HSBC Current from Xero"
- "Run the Trial Balance / Balance Sheet / P&L / BankSummary as at a date"
- "Look up a contact / invoice / payment"
- "Download invoice PDFs for the audit archive"
- The Xero connection broke / "do our API keys still work?" →
xero auth status
- Re-authorizing after a subscription lapse or revoked token →
xero auth url + exchange
Not for: writing/posting transactions as a system of record (use the accounting
ERP for that); the Xero Journals endpoint and Reports/BankStatement are
unavailable under the post-2026-03 granular-scope regime (see references/auth.md).
Setup (once per deployment)
$XERO_HOME (default ~/.config/xero-cli) holds config.json — secrets, never
committed. Copy scripts/config.example.json and fill in:
{
"client_id": "...", "client_secret": "...",
"redirect_uri": "http://localhost:1234/callback",
"tokens_path": "/abs/path/to/tokens.json",
"expected_tenant_id": "....-....", "expected_tenant_name": "ACME LTD"
}
tokens_path can point at an existing token store, so the CLI is a drop-in over
scripts that already manage tokens. expected_tenant_id arms the wrong-org guard.
Quick reference
| Command | Does |
|---|
xero auth status | Live /connections + active org + wrong-org/TRIAL guard (exit 1 on mismatch) |
xero auth url | Build the PKCE authorize URL; writes pending_auth.json |
xero auth exchange '<redirect-url>' | Finish OAuth; selects + pins the expected org |
xero auth refresh | Force a token refresh |
xero whoami | Org identity + financial-year end |
xero ratecheck | Remaining minute/day/app quota (one cheap call) |
xero get <Endpoint> [--where W] [--order O] [--all] | Generic GET; --all paginates |
xero report <Name> [--date D | --from D --to D] | Reports/ |
xero pdf <InvoiceID> <out.pdf> | Download an invoice PDF |
Reads guard the tenant automatically; --force bypasses (rarely correct).
The re-auth flow (recover a broken / wrong-org connection)
xero auth url → open the URL logged into Xero.
- Select the correct organisation (the whole point — a wrong pick is how you
end up on a trial org). Untick any others.
- Browser fails to load
…/callback?code=… (no listener — expected). Copy the
full address-bar URL.
xero auth exchange '<that-url>' → confirms tenants, pins the expected one.
xero auth status → must print Match: ✓ OK and a non-TRIAL class.
Programmatic use
import sys; sys.path.insert(0, ".../scripts")
from xero_client import XeroClient, bank_txns_for_fy
api = XeroClient()
api.assert_tenant()
txns = bank_txns_for_fy(api, "FY18-19", "<bank-account-id>")
tb = api.get("Reports/TrialBalance", {"date": "2026-03-31"})
See references/auth.md (OAuth2/PKCE, token lifecycle, multi-tenant, scope regime,
the wrong-org trap) and references/api.md (endpoints, where range filters,
reports, rate limits, FY helpers). Add commands/scripts per CONTRIBUTING.md.