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/callbackfor a tunnelled gateway, orhttp://localhost:7376/oauth/callbackfor a gateway on your own machine — notelocalhost, not the127.0.0.1form 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.
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 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
| Action | What it does | On by default |
|---|---|---|
invoices.list | Lists sales invoices and purchase bills | Yes |
invoices.read | Reads one invoice by InvoiceID | Yes |
contacts.list | Lists contacts, with search and filtering | Yes |
contacts.read | Reads one contact by ContactID | Yes |
accounts.list | Reads the chart of accounts | Yes |
bank_transactions.list | Lists spent and received money transactions | Yes |
reports.profit_and_loss | Runs the profit and loss report for a date range | Yes |
reports.balance_sheet | Runs the balance sheet report as at a date | Yes |
organisation.read | Reads which organisation this connection points at | Yes |
invoices.create | Creates one or more invoices | No, 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/jsonis pinned in the manifest. The Accounting API still serves XML when noAcceptheader asks otherwise, so this header is not optional decoration — without it an agent gets XML it cannot parse.- Create uses
PUT, notPOST. Xero's convention is the reverse of most APIs:PUT /Invoicescreates only and errors on a duplicate, whilePOST /Invoicesupdates or creates. The connector exposes thePUTform, soinvoices.createcannot 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-RemainingandX-AppMinLimit-Remaining; going over returns a 429 with aRetry-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.listandbank_transactions.listtake apageparameter and return 100 records a page.whereandordertake Xero's own filter syntax, for exampleStatus=="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.
fromDateandtoDate(profit and loss) anddate(balance sheet) are ISO dates. - Contact details are redacted.
EmailAddress,Phones,Addresses,BankAccountNumberandTaxNumberare 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
/connectionslike 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.