# Export AI spend to Xero

> Connect Xero by signing in, choose the accounts once, then close each month in one click as a draft manual journal or a draft bill, a line per team or cost centre.

Source: https://immiscible.fly.dev/docs/guides/xero

Each month's AI spend, allocated exactly as the chargeback on the Finance page shows it (with the unallocated line from the provider reconciliation, so it ties to the invoice), lands in Xero as one **draft** document for your finance team to review and approve. Nothing is posted without them.

## 1. Connect

Under **Settings**, **Connections**, **Connect** on the Xero card sends you to Xero to choose the organisation and approve. Immiscible asks for:

| Scope | Why |
|---|---|
| `accounting.manualjournals` | create the draft manual journal |
| `accounting.invoices` | create the draft bill, if you choose bills |
| `accounting.settings.read` | read the chart of accounts and the base currency |
| `accounting.contacts.read` | list suppliers for a bill |
| `offline_access` | stay connected (refresh tokens) |

These are Xero's granular scopes; apps registered since 2 March 2026 cannot be granted the older `accounting.transactions`. Tokens are sealed with the master key. Xero refresh tokens rotate on every use, and Immiscible seals each new one before it uses the access token that came with it.

## 2. Choose the accounts once

**Manage** opens the mapping, checked against your live chart of accounts:

- **Post as**: a draft manual journal (debit the expense account, credit an accrual account), or a draft bill (`ACCPAY`) from a supplier you pick.
- **A line per**: team, or cost centre. With a tracking category named, a team's cost centre becomes its tracking option.
- **Expense account**, with optional per-team overrides through the API (`PUT /api/w/:wid/accounting/xero/mapping`, `overrides`).

- **Currency**: the base currency at the ECB rate (month end, or the month's average), or, for a bill, USD as recorded. See below.

## Currency

AI spend is recorded in US dollars. The document posts in the organisation's base currency, converted at the **European Central Bank's euro reference rate**, crossed through the euro (GBP per USD is GBP per EUR divided by USD per EUR):

- **Month end** (the default): the last rate the ECB published in the month.
- **Average of the month**: the mean of every rate the ECB published in the month.

The rates come from the ECB's free daily XML ([eurofxref-hist-90d.xml](https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist-90d.xml), or [eurofxref-hist.xml](https://www.ecb.europa.eu/stats/eurofxref/eurofxref-hist.xml) for older months; [about the reference rates](https://www.ecb.europa.eu/stats/policy_and_exchange_rates/euro_reference_exchange_rates/html/index.en.html)) and are kept once fetched. A month is converted only after the ECB has published a rate for a later day, so its month-end rate is final. If the ECB cannot be reached, or the rate is not final yet, **nothing is posted**: the export is marked failed, says why, and is retried later.

Each line keeps its original amount in its description (for example `Alpha (USD 12.35 at 0.7576 GBP per USD)`). The document's total is the USD total converted once; if the lines, each rounded to the penny, add up to a penny or two more or less, one line headed **Rounding on conversion** makes up the difference, so the document always balances. The rate, its date (or the days averaged), and the ECB file it came from are kept on the export and in its `accounting_export` ledger record.

The ECB publishes these rates for information. If your policy needs another rate, choose a **bill in USD** instead (it needs multi-currency in your ledger), and convert there.

## 3. Close a month

**Close the month** exports the last full month (or the one you pick). It is idempotent:

1. A month already exported answers with what was exported, and nothing is sent.
2. Before creating, Immiscible looks in Xero for its reference, `IMM-YYYY-MM` (the journal's narration starts with it; a bill carries it as its number). One found is adopted, not duplicated.
3. Each create carries Xero's `Idempotency-Key` header.

A 429 or a 5xx is retried three times at once, then by a background retry with backoff (15 minutes, then doubling, five rounds). Every export, and every failure, is a ledger record (`accounting_export`). Disconnecting keeps the export history, so reconnecting cannot post a month twice.

## For the operator: register the Xero app once

1. At [developer.xero.com/app/manage](https://developer.xero.com/app/manage), **New app**, grant type **Auth Code** (a web app).
2. Redirect URI: `https://<your PUBLIC_URL host>/connect/xero/callback`. It must be https; Xero allows `http://localhost` for testing, not `127.0.0.1`.
3. Copy the client id and generate a secret into `XERO_CLIENT_ID` and `XERO_CLIENT_SECRET`. The Xero card appears once both are set.
4. Xero's free Starter tier allows 5 connected organisations; more needs a paid tier, and over 50 needs Xero's certification ([developer.xero.com/pricing](https://developer.xero.com/pricing)).

Sources: [OAuth 2.0 auth flow](https://developer.xero.com/documentation/guides/oauth2/auth-flow), [scopes](https://developer.xero.com/documentation/guides/oauth2/scopes), [granular scopes](https://developer.xero.com/faq/granular-scopes), [manual journals](https://developer.xero.com/documentation/api/accounting/manualjournals), [invoices](https://developer.xero.com/documentation/api/accounting/invoices), [idempotency](https://developer.xero.com/documentation/guides/idempotent-requests/idempotency), [limits](https://developer.xero.com/documentation/guides/oauth2/limits).
