A lightweight Python client for core Xero Accounting API workflows.
XeroConnect is not a full Xero SDK, ERP, or sync engine. It wraps a focused set of endpoints — OAuth, invoices, contacts, payments, and Profit & Loss reports — with a small API surface and a single runtime dependency (httpx).
XeroConnect is a good fit if you:
- Need a small, readable client for common accounting workflows in production systems
- Want plain JSON dict responses without generated model classes
- Already know which Xero endpoints you need and do not require full API coverage
- Prefer a minimal dependency footprint (
httpxonly) - Are building scripts, internal tools, or backend services that list invoices, fetch contacts, read payments, or pull P&L reports
XeroConnect is not the right choice if you:
- Need broad Xero API coverage (payroll, projects, assets, files, bank feeds, and more)
- Want official Xero-maintained SDKs with OpenAPI-generated models and published API reference docs
- Are building a full Xero App Store application and need sample apps, helper methods, and platform-specific integration patterns
- Require PKCE OAuth flows for desktop or mobile apps (not supported)
- Need create endpoints for all resources (invoice create, contact create, and payment create are not included)
Several Python libraries exist for Xero. Here is how they differ in scope and design.
| xero-python | pyxero | xerosdk | XeroConnect | |
|---|---|---|---|---|
| Maintainer | Xero Developer API (official) | Community | Third-party (Fyle) | Independent open source |
| API coverage | Full — Accounting, Payroll (AU/UK/NZ), Projects, Assets, Files | Accounting endpoints via dict API | Subset — Invoices, Contacts, Accounts, Items, Tracking | Focused subset — OAuth, Invoices, Contacts, Payments, P&L |
| Response format | Generated model classes | Dicts | Class-based wrappers | Dicts |
| HTTP library | urllib3 (via generated client) |
requests |
requests |
httpx |
| OAuth | Built-in token management helpers | OAuth2 with PKCE support | Refresh token on connection | Separate OAuthClient |
| Package size | Large (OpenAPI-generated) | Moderate | Small | Small |
Use XeroConnect when your integration only needs the endpoints it supports, you value a small install and a simple resource-based API (client.invoices.list()), and plain dict responses are sufficient for your application.
Use the official Xero Python SDK when you need full API coverage across Accounting and other Xero API sets (Payroll, Projects, Assets, Files), typed model classes generated from Xero's OpenAPI specification, official documentation and sample applications, or long-term alignment with Xero's published SDK conventions.
- pyxero — established community library with dict-based access to accounting endpoints and PKCE OAuth support. A reasonable choice if you need broader accounting CRUD with a dict-oriented API.
- xerosdk — small third-party SDK covering a limited set of resource classes with built-in token refresh on the connection object.
XeroConnect is a focused Python client for production accounting workflows — OAuth, invoices, contacts, payments, and Profit & Loss reports — without the scope of a full API SDK.
| Area | Methods |
|---|---|
| OAuth | Authorization URL, token exchange, refresh token, connections, tenant ID helper |
| Invoices | List, get, update (with optional payload wrapping) |
| Contacts | List, get |
| Payments | List, get |
| Reports | Profit and Loss |
| Webhooks | Signature verification |
| Helpers | Date filter builder, access token provider hook |
| Report | Method |
|---|---|
| Profit & Loss | client.reports.profit_and_loss() |
Install from PyPI:
pip install xeroconnectFor development:
git clone https://github.com/alixaprodev/xeroconnect.git
cd xeroconnect
pip install -e ".[dev]"Use OAuthClient to handle the authorization flow separately from API calls:
from xeroconnect import OAuthClient
oauth = OAuthClient(
client_id="YOUR_CLIENT_ID",
client_secret="YOUR_CLIENT_SECRET",
redirect_uri="https://yourapp.com/callback",
)
# Step 1: Redirect the user to this URL
auth_url = oauth.get_authorization_url(
scopes=[
"openid",
"profile",
"email",
"accounting.transactions",
"accounting.contacts",
"offline_access",
],
state="random-state-string",
)
# Step 2: Exchange the authorization code for tokens
tokens = oauth.exchange_code("authorization-code-from-callback")
access_token = tokens["access_token"]
refresh_token = tokens["refresh_token"]
# Step 3: Resolve tenant ID from connections
tenant_id = oauth.get_tenant_id(access_token)
# Or inspect all connected organisations
connections = oauth.get_connections(access_token)
# Step 4: Refresh when the access token expires
tokens = oauth.refresh_token(refresh_token)
# Xero rotates refresh tokens — persist the new refresh_token from the responseToken requests use HTTP Basic authentication (client_id:client_secret in the Authorization header) with a form-encoded body. This follows RFC 6749 and is supported by Xero alongside credential-in-body approaches.
For common accounting workflows (invoices, contacts, payments, reports):
scopes = [
"openid",
"profile",
"email",
"accounting.transactions",
"accounting.transactions.read",
"accounting.contacts",
"accounting.contacts.read",
"accounting.reports.read",
"accounting.settings",
"accounting.settings.read",
"offline_access",
]from xeroconnect import XeroClient
client = XeroClient(
access_token="your-access-token",
tenant_id="your-tenant-id",
)
# Use as a context manager to ensure connections are closed
with XeroClient(access_token="...", tenant_id="...") as client:
invoices = client.invoices.list()XeroConnect does not store tokens. Use an access_token_provider so the client can recover from expired access tokens:
from xeroconnect import OAuthClient, XeroClient
oauth = OAuthClient(client_id="...", client_secret="...", redirect_uri="...")
def load_token():
... # load from your database, cache, or secrets manager
def save_token(tokens: dict):
... # persist access_token, refresh_token, and expiry
def get_access_token() -> str:
token = load_token()
if token.is_expired():
tokens = oauth.refresh_token(token.refresh_token)
save_token(tokens)
return tokens["access_token"]
return token.access_token
client = XeroClient(
access_token=get_access_token(),
tenant_id=oauth.get_tenant_id(get_access_token()),
access_token_provider=get_access_token,
)On HTTP 401, the transport calls access_token_provider once and retries the request.
# Before
import requests
response = requests.get(
f"https://api.xero.com/api.xro/2.0/Invoices/{invoice_id}",
headers={
"Authorization": f"Bearer {access_token}",
"xero-tenant-id": tenant_id,
"Accept": "application/json",
},
)
response.raise_for_status()
invoice_data = response.json()["Invoices"][0]
# After
from xeroconnect import XeroClient
client = XeroClient(access_token=access_token, tenant_id=tenant_id)
invoice_data = client.invoices.get(invoice_id)["Invoices"][0]from datetime import date
from xeroconnect import XeroClient, date_on_or_after
# List invoices with filtering and pagination
invoices = client.invoices.list(
where=date_on_or_after(date(2026, 1, 1)),
order="Date DESC",
page=1,
page_size=100,
offset=0,
)
# Get a single invoice
invoice = client.invoices.get("invoice-id")
print(invoice["Invoices"][0]["InvoiceNumber"])
# Update with full Xero envelope
result = client.invoices.update("invoice-id", {
"Invoices": [{
"InvoiceID": "invoice-id",
"Status": "AUTHORISED",
}],
})
# Update with bare invoice dict (auto-wrapped when wrap=True)
result = client.invoices.update(
"invoice-id",
{"InvoiceID": "invoice-id", "Status": "AUTHORISED"},
)Xero returns pagination metadata in response headers (for example x-page-count), not in the JSON body. List methods return the JSON body only. To read pagination headers, pass a custom httpx.Client and inspect the raw response via your own wrapper if needed.
contacts = client.contacts.list(page=1, page_size=100)
contact = client.contacts.get("contact-id")
print(contact["Contacts"][0]["Name"])payments = client.payments.list(page=1)
payments = client.payments.list(
where='Status=="AUTHORISED"',
order="Date DESC",
page_size=50,
offset=100,
)
payment = client.payments.get("payment-id")report = client.reports.profit_and_loss(
from_date="2026-01-01",
to_date="2026-01-31",
)
# With optional parameters
report = client.reports.profit_and_loss(
from_date="2026-01-01",
to_date="2026-01-31",
periods=3,
timeframe="MONTH",
tracking_category_id="category-id",
tracking_option_id="option-id",
standard_layout=True,
payments_only=False,
)Verify incoming webhook payloads using the signing key from your Xero app:
from xeroconnect import verify_signature
is_valid = verify_signature(
payload=request.body, # raw bytes
signature_header=request.headers["x-xero-signature"],
webhook_key="your-webhook-key",
)XeroConnect raises typed exceptions with useful context:
from xeroconnect import (
XeroClient,
XeroAPIError,
XeroAuthError,
XeroConnectionError,
XeroRateLimitError,
XeroValidationError,
)
client = XeroClient(access_token="...", tenant_id="...")
try:
invoices = client.invoices.list()
except XeroAuthError as e:
print(f"Auth failed: {e.message}")
except XeroRateLimitError as e:
print(f"Rate limited: {e.xero_message}, retry after {e.retry_after}s")
except XeroValidationError as e:
print(f"Validation errors: {e.validation_errors}")
except XeroConnectionError as e:
print(f"Network error: {e.message}")
except XeroAPIError as e:
print(f"API error {e.status_code} on {e.endpoint}")
print(f"Response: {e.response_body}")| Exception | When |
|---|---|
XeroConnectError |
Base exception |
XeroConnectionError |
Network timeouts and connection failures |
XeroAuthError |
OAuth failures and HTTP 401 (expired or invalid token) |
XeroAPIError |
General API errors |
XeroRateLimitError |
HTTP 429 rate limit (retry_after when provided) |
XeroValidationError |
HTTP 400 validation errors (validation_errors when available) |
- Focused scope — Only the endpoints listed above are supported. See xero-python for full API coverage.
- No models — Responses are plain JSON dictionaries.
- No token storage — You manage access and refresh tokens yourself.
- No webhook event parsing — Signature verification only; event handling is your responsibility.
- No sync engine — No background workers or database integration.
- No framework bindings — No Django or FastAPI integration.
- Limited write operations — Invoice update is supported; invoice create, contact create, and payment create are not.
- No async client — Synchronous
httpxonly in v0.2.
- Generic GET helper for additional read endpoints
- Items API
- Optional 429 retry with
Retry-After - Additional reports (Balance Sheet, Trial Balance)
- Invoice, contact, and payment create endpoints
- Async client (
httpx.AsyncClient) - PKCE OAuth flow
- Python >= 3.9
- httpx >= 0.27
MIT License — see LICENSE for details.
See CONTRIBUTING.md for setup, testing, and pull request guidelines.
Release history is in CHANGELOG.md.
H. Ali — haxratali0@gmail.com