Skip to main content

Xero

The Xero connector reaches the Accounting API v2.0 for one Xero organisation. It is good for asking an agent accounting questions you would otherwise open Xero to answer: what is outstanding, who owes it, what the chart of accounts looks like, what money moved through the bank, and what the profit and loss says for a period. It can also create an invoice, but that action ships switched off and approval-gated.

Two things about Xero are unusual, and both will stop you before you get a single successful call. Read them before you install.

What you need

Xero has no API keys. Every request is authenticated with an OAuth 2.0 access token, so you register your own app and the gateway obtains tokens from it on your behalf.

1. Register an app. Sign in at developer.xero.com/app/manage and choose New app. Pick Web app: the gateway runs the authorisation code flow itself and needs a client id and a client secret, which the Mobile or desktop app type does not issue. PKCE is added automatically where the connector's manifest records that the provider supports it.

2. Set the redirect URI. Xero requires HTTPS, with one exception: http://localhost is accepted for testing, and http://127.0.0.1 is not — Xero rejects it outright. So use:

  • https://<your-gateway-host>/oauth/callback for a tunnelled gateway, or
  • http://localhost:7376/oauth/callback for a gateway on your own machine — note localhost, not the 127.0.0.1 form used elsewhere in these docs.

You will type this string in two places, here and in the redirect_uri field of the start call below, and the two must match character for character. A mismatch is the most common way this fails.

3. Request the least privilege you need. The connector's manifest advertises the read set plus refresh:

offline_access, accounting.transactions.read, accounting.contacts.read, accounting.settings.read, accounting.reports.read

offline_access is what gets you a refresh token; without it you get an access token and nothing to renew it with, and the gateway refuses to seal a credential it cannot keep alive. Add the read-write accounting.transactions scope only if you intend to switch on Create invoice.

Connector OAuth needs gateway 1.1.0 or later

Your gateway runs the whole authorisation code flow. It hands you a link, exchanges the code Xero sends back, and seals the access token, the refresh token and your client secret as the connection's credential. permaura.com is never in the path and never sees any of it. An older gateway has no oauth/start endpoint at all; curl -s http://127.0.0.1:7376/health reports the version you are running.

From then on the connection stays up on its own. A Xero access token still lives for 30 minutes, but the gateway renews it two minutes before it expires. The subtle part is that Xero rotates the refresh token on every renewal and invalidates the old one; the gateway persists each rotated token, which is what stops the connection locking itself out. A refresh token expires only if it goes 60 days unused.

The console has no button for this yet, so today you start the flow through the gateway's REST API, as below.

4. Get your tenant id. Every Accounting API call needs a Xero-tenant-id header naming the organisation, and you need it before you create the connection — so do this one lookup by hand, with an access token you obtain yourself from the app you just registered. The gateway seals the tokens it obtains and never shows them to you; this throwaway token is only for reading /connections:

curl -s https://api.xero.com/connections \
-H "Authorization: Bearer $XERO_ACCESS_TOKEN" \
-H 'Content-Type: application/json'

Each entry in the response has a tenantId (a UUID) and a tenantName. Take the id of the organisation you mean to reach.

The connection will 403 until you add the tenant id

The tenant id is per-connection, not per-connector — two people installing this connector point at two different organisations — so it is deliberately not in the manifest. A placeholder there would be worse than nothing.

Today there is nowhere in the Add connection flow to supply it: POST /v1/connections/from-preset accepts a name and a label and no extra headers. So a connection created straight from the catalog sends no Xero-tenant-id, and every call comes back 403 Forbidden.

Supply it when you create the connection. POST /v1/connections/from-preset takes an optional static_headers, merged over the manifest's own for that one connection:

BASE=http://127.0.0.1:7376
curl -X POST $BASE/v1/connections/from-preset -H "authorization: Bearer $TOKEN" \
-H 'content-type: application/json' -d '{
"preset_id": "xero",
"name": "Acme Ltd",
"static_headers": { "Xero-tenant-id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
}'

The response echoes back the host the connection is pinned to and whether it differs from the published manifest. Then authorise the returned connection_id as below. The gateway refuses a credential-bearing header name here, because a secret belongs in the sealed credential rather than in a connection record stored in the clear.

The Add connection form in the console does not offer this field yet, so today the tenant id has to be set through the API.

Add the connection

Both halves of this are API-side today: the tenant id has no field in the Add connection form, and the console does not yet expose the OAuth flow. So create the connection as above, then authorise it against the connection_id that came back.

Ask the gateway for an authorisation link, passing the client id and secret from your Xero app and the redirect URI you registered:

curl -X POST $BASE/v1/connections/$CONNECTION_ID/oauth/start \
-H "authorization: Bearer $TOKEN" -H 'content-type: application/json' -d '{
"client_id": "<your-xero-client-id>",
"client_secret": "<your-xero-client-secret>",
"redirect_uri": "http://localhost:7376/oauth/callback"
}'

The manifest's scopes are used unless you pass your own scopes, which you would do if you added accounting.transactions for invoice creation. Open the authorize_url from the response in a browser, choose the same organisation whose tenant id you pinned, and approve. Xero redirects back to your gateway, which exchanges the code, seals the credential and shows a plain Connected page. The link is single-use and expires after ten minutes.

If a device is enrolled for approvals, run this from the gateway's own host rather than over a tunnel. Sealing a credential changes the upstream identity the connection acts as, which is a mutation held for a device signature, and a browser redirect cannot carry one — so the gateway refuses to start the flow over the broker in that setup.

The credential is sealed on your gateway and never displayed again. Nothing can use it until a grant allows it.

What the agent can do

ActionWhat it doesOn by default
invoices.listLists sales invoices and purchase billsYes
invoices.readReads one invoice by InvoiceIDYes
contacts.listLists contacts, with search and filteringYes
contacts.readReads one contact by ContactIDYes
accounts.listReads the chart of accountsYes
bank_transactions.listLists spent and received money transactionsYes
reports.profit_and_lossRuns the profit and loss report for a date rangeYes
reports.balance_sheetRuns the balance sheet report as at a dateYes
organisation.readReads which organisation this connection points atYes
invoices.createCreates one or more invoicesNo, and needs approval

organisation.read is the quickest way to confirm you pinned the right tenant: call it first and check the name that comes back is the organisation you meant.

Good to know

  • Accept: application/json is pinned in the manifest. The Accounting API still serves XML when no Accept header asks otherwise, so this header is not optional decoration — without it an agent gets XML it cannot parse.
  • Create uses PUT, not POST. Xero's convention is the reverse of most APIs: PUT /Invoices creates only and errors on a duplicate, while POST /Invoices updates or creates. The connector exposes the PUT form, so invoices.create cannot quietly overwrite an existing invoice. The body is {"Invoices": [...]}, an array even for one.
  • Rate limits are tight and per organisation. 60 calls per minute and 5,000 per day per tenant, 5 concurrent calls, and 10,000 calls per minute across all tenants of your app. Every response carries X-MinLimit-Remaining, X-DayLimit-Remaining and X-AppMinLimit-Remaining; going over returns a 429 with a Retry-After. The 5,000 applies to apps on Xero's Core tier and above; the Starter tier gets 1,000 a day. An agent looping over pages will exhaust the minute limit quickly.
  • Lists are paged at 100. invoices.list, contacts.list and bank_transactions.list take a page parameter and return 100 records a page. where and order take Xero's own filter syntax, for example Status=="AUTHORISED".
  • Reports are shaped for humans, not machines. The profit and loss and balance sheet come back as nested rows of cells matching the on-screen report, not as flat figures. fromDate and toDate (profit and loss) and date (balance sheet) are ISO dates.
  • Contact details are redacted. EmailAddress, Phones, Addresses, BankAccountNumber and TaxNumber are stripped from responses before the agent sees them. The agent can tell you a contact owes £3,400 across four invoices without learning their bank details.
  • Try it on the demo company first. Every Xero account has a demo organisation with sample data, and it appears in /connections like any other tenant. Point the connector at that tenant id while you are working out what to enable.
  • The kill switch is instant. Disabling the connection in the console refuses every call through it immediately, across every agent and grant, without unpicking individual permissions.