Skip to main content
Technical Preview

Overview

A client-mode connector for the Xero Accounting API (api.xro/2.0). Use it to connect the shop floor to accounting: raise a sales invoice when a batch ships, keep the item catalogue in step with your MES, and read item stock levels, open invoices or customers into MQTT. The file holds two connectors. XeroAccountingRestApi talks to https://api.xero.com (accounting calls and tenant discovery), and XeroOAuth gets access tokens from Xero’s identity host. Every accounting call sends Accept: application/json (Xero answers XML otherwise) and the xero-tenant-id header. Topics use finance/xero/<object>/<verb>; replies land on <topic>/response. See the base guide: REST API Connectors.
Auth: Xero uses OAuth 2.0, in one of two ways.
  • Web app (authorization code). Do the one-time consent outside the broker, asking for offline_access plus the accounting scopes you need, to get a refresh token. Then call GetConnections once to find the organisation’s tenantId. After that the broker can stay connected on its own: publish to RefreshToken to get a fresh access token (valid 30 minutes).
  • Custom connection (client credentials). A custom connection is linked to one organisation and has no refresh token. Publish to ClientCredentialsToken with the scopes to get a 30-minute access token. Custom connections are a paid Xero option, but you can try one for free with the Demo Company.
Every accounting call then takes access_token and tenant_id from the payload. Keep the client id, client secret and refresh token in Hub secrets (suggested names XERO_CLIENT_ID, XERO_CLIENT_SECRET, XERO_BASIC_AUTH, XERO_REFRESH_TOKEN, XERO_TENANT_ID). Never commit them into the .lot.
The refresh token rotates. Each refresh returns a new refresh_token, and the old one stops working. Always store the latest value. A refresh token that is not used for 60 days expires, and the user has to connect again.
Scopes. Xero apps created on or after 2 March 2026 only get granular scopes. For this template ask for accounting.invoices (invoices and items), accounting.contacts and accounting.settings (organisation and items), plus offline_access for a refresh token. Older apps can keep the broad accounting.transactions scope until September 2027. A call without the right scope fails with 401 or 403.

Basic Syntax

MQTT examples

Control fields (access_token, tenant_id, ids, where, page) only fill the URL and headers. Only body is sent as the request body. RefreshToken (finance/xero/oauth/refresh). basic_auth is the Base64 of client_id:client_secret. Expect 200:
Response:
ClientCredentialsToken (finance/xero/oauth/token), custom connections only. The scope list is space-separated, so URL-encode it:
GetConnections (finance/xero/connections/get) needs only the token. Response:
GetItems (finance/xero/item/list). The where filter is placed in the URL, so URL-encode it. This asks for tracked inventory items:
Response (trimmed):
Other filters: Code=="PUMP-SEAL" becomes Code%3D%3D%22PUMP-SEAL%22. UpdateOrCreateItems (finance/xero/item/upsert). Creates the item, or updates it when the Code already exists. Expect 200 with the item:
A tracked item needs an inventory asset account and a cost of goods sold account. QuantityOnHand is read only: stock goes up with approved purchase bills (ACCPAY) and down with approved sales invoices (ACCREC). Use CreateItems (PUT) when a repeated Code should fail instead of update. CreateInvoices (finance/xero/invoice/create). Bills a customer for two seal kits from batch 4711. Expect 200 with the new invoice (InvoiceID, InvoiceNumber, Total):
Leave out Status (or send DRAFT) to create a draft for review, then approve it later with UpdateOrCreateInvoices: {"Invoices":[{"InvoiceID":"<id>","Status":"AUTHORISED"}]}. EmailInvoice (finance/xero/invoice/email) takes invoice_id. The invoice must be an approved sales invoice and the contact needs an email address. Xero answers 204 with an empty body. GetInvoices and GetContacts take where and page (start at 1), for example "where": "Status%3D%3D%22AUTHORISED%22", "page": 1.

Keeping the token

A pair of small actions can refresh the token from Hub secrets and keep the latest values when the reply arrives:
Refresh before the access token expires (it lasts 30 minutes), or when a call returns 401 AuthenticationUnsuccessful. Restrict who can read the finance/xero/# topics with a rule, because these payloads carry tokens and client credentials.

Webhooks (companion, not packaged)

Xero webhooks POST events (for example invoice or contact created or updated) to an HTTPS URL. Each request carries an x-xero-signature header, an HMAC-SHA256 of the body with your webhook key, and Xero first checks the URL with an “intent to receive” handshake. Receiving them needs a separate ENABLE_SERVER REST API connector on a publicly reachable HTTPS address. That companion connector is not part of this template. Polling GetInvoices with an UpdatedDateUTC filter is the alternative.

Notes

  • Limits per organisation: 5 calls at a time, 60 calls per minute and 5,000 calls per day (1,000 per day on Xero’s free Starter developer tier). Across all organisations an app can make 10,000 calls per minute. Above that Xero answers 429 with a Retry-After header. Each reply carries X-MinLimit-Remaining and X-DayLimit-Remaining.
  • PUT and POST take up to 50 records per call. By default one invalid record fails the whole request with 400.
  • Common errors: 401 AuthenticationUnsuccessful (expired or invalid token, or a missing scope), 403 (the token is not connected to that tenant_id), 400 ValidationException with the reason under Elements[].ValidationErrors, 404 (unknown id), 429 (rate limit).
  • Add an Idempotency-Key header to a write event if the broker may retry it; Xero then ignores repeats of the same request.
  • Not covered: invoice PDF download (binary), attachments, the Payroll, Projects and Assets APIs, disconnecting a tenant (DELETE /connections/{id}) and token revoke (POST https://identity.xero.com/connect/revocation).
  • Suggested branches: feat/rest-api-route-xero, feat/docs-rest-api-xero (Hugo pushes).
Last modified on October 21, 2026