Metadata-Version: 2.4
Name: xeroconnect
Version: 0.2.0
Summary: A lightweight Python client for core Xero Accounting API workflows
Project-URL: Homepage, https://github.com/alixaprodev/xeroconnect
Project-URL: Documentation, https://github.com/alixaprodev/xeroconnect#readme
Project-URL: Repository, https://github.com/alixaprodev/xeroconnect
Project-URL: Issues, https://github.com/alixaprodev/xeroconnect/issues
Project-URL: Changelog, https://github.com/alixaprodev/xeroconnect/blob/main/CHANGELOG.md
Project-URL: PyPI, https://pypi.org/project/xeroconnect/
Author-email: "H. Ali" <haxratali0@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: accounting,api,invoices,oauth,xero
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Accounting
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.0; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Description-Content-Type: text/markdown

# XeroConnect

[![PyPI version](https://img.shields.io/pypi/v/xeroconnect.svg)](https://pypi.org/project/xeroconnect/)
[![Python versions](https://img.shields.io/pypi/pyversions/xeroconnect.svg)](https://pypi.org/project/xeroconnect/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)

A lightweight Python client for core [Xero Accounting API](https://developer.xero.com/documentation/api/api-overview) 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`).

## Who This Package Is For

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** (`httpx` only)
- Are building scripts, internal tools, or backend services that list invoices, fetch contacts, read payments, or pull P&L reports

## Who This Package Is Not For

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)

## XeroConnect vs Other Python Libraries

Several Python libraries exist for Xero. Here is how they differ in scope and design.

| | [xero-python](https://github.com/XeroAPI/xero-python) | [pyxero](https://github.com/hugovk/pyxero) | [xerosdk](https://pypi.org/project/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 |

### When to Use XeroConnect

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.

### When to Use the Official SDK (`xero-python`)

Use the [official Xero Python SDK](https://github.com/XeroAPI/xero-python) 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.

### When to Use Other Libraries

- **[pyxero](https://github.com/hugovk/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](https://pypi.org/project/xerosdk/)** — small third-party SDK covering a limited set of resource classes with built-in token refresh on the connection object.

## Positioning

> **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.**

## What It Supports

### Core Endpoints

| 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 |

### Reports

| Report | Method |
|--------|--------|
| Profit & Loss | `client.reports.profit_and_loss()` |

## Installation

Install from [PyPI](https://pypi.org/project/xeroconnect/):

```bash
pip install xeroconnect
```

For development:

```bash
git clone https://github.com/alixaprodev/xeroconnect.git
cd xeroconnect
pip install -e ".[dev]"
```

## OAuth Flow

Use `OAuthClient` to handle the authorization flow separately from API calls:

```python
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 response
```

Token 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.

### Recommended OAuth scopes

For common accounting workflows (invoices, contacts, payments, reports):

```python
scopes = [
    "openid",
    "profile",
    "email",
    "accounting.transactions",
    "accounting.transactions.read",
    "accounting.contacts",
    "accounting.contacts.read",
    "accounting.reports.read",
    "accounting.settings",
    "accounting.settings.read",
    "offline_access",
]
```

## Basic Client Usage

```python
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()
```

### Token refresh pattern

XeroConnect does not store tokens. Use an `access_token_provider` so the client can recover from expired access tokens:

```python
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.

### Migrating from raw HTTP

```python
# 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]
```

## Invoices

```python
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"},
)
```

### Pagination

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

```python
contacts = client.contacts.list(page=1, page_size=100)

contact = client.contacts.get("contact-id")
print(contact["Contacts"][0]["Name"])
```

## Payments

```python
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")
```

## Profit and Loss Report

```python
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,
)
```

## Webhooks

Verify incoming webhook payloads using the signing key from your Xero app:

```python
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",
)
```

## Error Handling

XeroConnect raises typed exceptions with useful context:

```python
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) |

## Limitations

- **Focused scope** — Only the endpoints listed above are supported. See [xero-python](https://github.com/XeroAPI/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 `httpx` only in v0.2.

## Roadmap

- [ ] 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

## Requirements

- Python >= 3.9
- httpx >= 0.27

## License

MIT License — see [LICENSE](LICENSE) for details.

## Contributing

See [CONTRIBUTING.md](CONTRIBUTING.md) for setup, testing, and pull request guidelines.

Release history is in [CHANGELOG.md](CHANGELOG.md).

## Author

H. Ali — [haxratali0@gmail.com](mailto:haxratali0@gmail.com)

- PyPI: [https://pypi.org/project/xeroconnect/](https://pypi.org/project/xeroconnect/)
- GitHub: [https://github.com/alixaprodev/xeroconnect](https://github.com/alixaprodev/xeroconnect)
