/api.xro/2.0, with Xero’s JSON envelope, /Date(...)/ dates, and ValidationException errors. It covers the organisation, the chart of accounts and tax rates, contacts and contact groups, items with tracked inventory, invoices and bills with Xero’s status transitions and computed totals, credit notes and allocations, payments, bank transactions, and attachments. Organisations are tenants: every call names one in xero-tenant-id, checked against the token’s connections. OAuth 2.0 runs through an authorize page that picks a user and the organisations to connect, with PKCE, rotating refresh tokens, custom connections (client credentials), and OpenID Connect. Webhooks pass intent to receive before any event is sent, and every delivery is signed with x-xero-signature. The tests drive it with the official xero-node SDK (20.0.0).
At a glance
Start
--service. When it is the only enabled service, it listens on http://localhost:4000. A fresh emulator has the organisation Demo Company (NZ) (tenant 4a8d6f2e-3b1c-4e5a-9d7f-1c2b3a4d5e6f), the user [email protected] (who can reach every organisation), and the app C0FFEE00BA5E11E0000000000000A001 / baseline_xero_client_secret_0000000000000000000 with redirect URIs http://localhost:3000/callback and http://localhost:5000/callback.
xero-node takes the accounting base URL through accountingApi.basePath. It hard codes the identity and connections hosts, so a test that uses initialize(), updateTenants(), refreshWithRefreshToken(), or getClientCredentialsToken() has to send https://identity.xero.com and https://api.xero.com to the emulator (for example with an axios request interceptor and a wrapped Issuer.discover from xero-node’s own openid-client). Calls made with a token you already hold need only basePath:
URL mapping
Auth
Every API call needsAuthorization: Bearer {access_token}. Access tokens come from the OAuth flow, from access_tokens[] in seed config (fixed tokens that never expire), from POST /_xero/tokens, or from the CLI (tokens the CLI accepts reach every organisation with every scope).
- No token: 401
{"Type":null,"Title":"Unauthorized","Status":401,"Detail":"AuthenticationUnsuccessful","Instance":"<xero-correlation-id>","Extensions":{}}withWWW-Authenticate: Bearer. An unknown, revoked, or expired token answers the same withBearer error=invalid_token. xero-tenant-idmissing, malformed, or not connected to the token: 403 withTitle: "Forbidden". Custom connection tokens may leave the header out.- A token without the endpoint’s scope: 401
Detail: "AuthorizationUnsuccessful"withWWW-Authenticate: insufficient_scope. Broad scopes (accounting.transactions,accounting.contacts,accounting.settings,accounting.attachments) and the granular ones (accounting.invoices,accounting.payments,accounting.banktransactions) both work;.readscopes allowGETonly.
Connections
GET /connections— List connections: the token’s connections (id,authEventId,tenantId,tenantType: "ORGANISATION",tenantName,createdDateUtc,updatedDateUtc), filtered with?authEventId=DELETE /connections/{id}— Delete connection: disconnects one (204, or 404)
Rate limits
Every response carriesxero-correlation-id, and X-DayLimit-Remaining, X-MinLimit-Remaining, and X-AppMinLimit-Remaining against Xero’s limits (60 a minute and 5,000 a day per tenant, 10,000 a minute per app). With rate_limits.enforce: true a call past a limit answers 429 with an empty body, Retry-After, and X-Rate-Limit-Problem (minute, day, or appminute).
OAuth 2.0 and OpenID Connect
GET /identity/connect/authorize— Authorize:response_type=code,client_id,redirect_uri(must be registered),scope,state, and optionalnonceandcode_challengewithcode_challenge_method=S256(required for apps without a secret). The page asks which user signs in and, for accounting scopes, which of the user’s organisations to connect; each becomes a connection. The callback getscode,scope,state, andsession_state, orerror(invalid_scope,access_denied,unsupported_response_type)POST /connect/token— Get token: client credentials in Basic auth or the body, and the grantsauthorization_code(codes are single use and live 5 minutes),refresh_token(rotates; the old token keeps working foroauth.refresh_grace_seconds, 30 minutes by default; refresh tokens live 60 days), andclient_credentialsfor apps with acustom_connection. Access tokens are RS256 JWTs that live 30 minutes, with Xero’s claims (client_id,sub,xero_userid,authentication_event_id,global_session_id,scope). Withopenid, an ID token (5 minutes) carriesemail,given_name,family_name, andnonce. A refresh token needsoffline_access. Errors are 400{"error":"invalid_client"},invalid_grant,unsupported_grant_type,unauthorized_client, orinvalid_scopePOST /connect/revocation— Revoke token: with a refresh token, it revokes it and removes the user’s connections to the appPOST /connect/introspect— Introspect tokenGET /connect/userinfo— Get user infoGET /connect/endsession— End sessionGET /.well-known/openid-configuration— OpenID configuration: keys at/.well-known/openid-configuration/jwks; both also under/identity
Accounting API
All accounting routes are under/api.xro/2.0.
Organisation
GET /Organisation— Get organisation
Accounts
GET /Accounts— List accountsGET /Accounts/{id}— Get accountPUT /Accounts— Create account: one accountPOST /Accounts/{id}— Update accountDELETE /Accounts/{id}— Delete account: unused, non-system accounts
Tax rates
GET /TaxRates— List tax rates: the region’s rates (NZ 15% GST, AU 10% GST, UK 20% VAT; the US set is a Baseline simplification)GET /TaxRates/{TaxType}— Get tax rate
Contacts
POST /Contacts— Create or update contacts: the same path takesGETto list andPUTto createGET /Contacts/{id or ContactNumber}— Get contactPOST /Contacts/{id}— Update contact
ContactStatus: "ARCHIVED" (lists leave archived contacts out unless includeArchived=true). Names are unique among active contacts.
Contact groups
PUT /ContactGroups— Create contact group:GETon the same path lists themPOST /ContactGroups/{id}— Update contact group: rename, orStatus: "DELETED";GETon the same path reads onePUT /ContactGroups/{id}/Contacts— Add contacts to groupDELETE /ContactGroups/{id}/Contacts— Remove all contacts from groupDELETE /ContactGroups/{id}/Contacts/{ContactID}— Remove contact from group
Items
POST /Items— Create or update items: the same path takesGETto list andPUTto createPOST /Items/{id or Code}— Update item:GETon the same path reads oneDELETE /Items/{id}— Delete item: 204
INVENTORY asset account and PurchaseDetails.COGSAccountCode; QuantityOnHand and TotalCostPool follow authorised bills and invoices. Selling more than is on hand fails with Insufficient stock. Items are eligible for backorder. unless allowBackorders=true.
Invoices
POST /Invoices— Create or update invoices: the same path takesGETto list andPUTto createPOST /Invoices/{id or InvoiceNumber}— Update invoice:GETon the same path reads one
ACCREC and ACCPAY; LineAmountTypes Exclusive, Inclusive, or NoTax; totals, tax, and AmountDue computed; automatic INV-0001 numbers; transitions DRAFT to SUBMITTED, AUTHORISED, or DELETED, SUBMITTED back to DRAFT, AUTHORISED to VOIDED; paid invoices accept only the fields Xero allows. Line items are updated, added, and removed by LineItemID. A contact named only by Name is created.
Credit notes
POST /CreditNotes— Create or update credit notes: the same path takesGETto list andPUTto createPOST /CreditNotes/{id}— Update credit note:GETon the same path reads onePUT /CreditNotes/{id}/Allocations— Allocate credit noteDELETE /CreditNotes/{id}/Allocations/{AllocationID}— Delete allocation
Payments
POST /Payments— Create payments: the same path takesGETto list andPUTto createGET /Payments/{id}— Get paymentPOST /Payments/{id}— Reverse payment: with{"Status":"DELETED"}
AUTHORISED documents from a bank account (or one with EnablePaymentsToAccount), may be partial, and may not exceed the amount due; a fully paid invoice turns PAID.
Bank transactions
POST /BankTransactions— Create or update bank transactions: the same path takesGETto list andPUTto createPOST /BankTransactions/{id}— Update bank transaction:GETon the same path reads one
SPEND and RECEIVE, Status: "DELETED" to remove.
Attachments
Attachments work on invoices, credit notes, bank transactions, contacts, and accounts:GET /{Type}/{id}/Attachments— List attachmentsGET /{Type}/{id}/Attachments/{AttachmentID or FileName}— Get attachment: the filePOST /{Type}/{id}/Attachments/{FileName}— Upload attachment:PUTorPOSTwith the raw file as the body (IncludeOnline=trueon invoices and credit notes)
Lists and filters
wheretakes Xero’s Dynamic LINQ syntax:==,!=,<,>,<=,>=,AND,OR,NOT,&&,||, parentheses, nested fields (Contact.Name),.Contains(),.StartsWith(),.EndsWith(),.ToLower(),.ToUpper(),guid("..."), andDateTime(2026, 1, 31). String comparisons ignore case. A bad expression answers 400QueryParseExceptionwith Dynamic LINQ’s message (No property or field 'X' exists in type 'Invoice')ordertakes a comma list of fields withASCorDESCIf-Modified-Sincereturns records changed after the timeIDs,Statuses,InvoiceNumbers,ContactIDs,createdByMyApp,includeArchived, andsearchTerm(contacts and invoices)pageandpageSize(100 by default, up to 1,000) on invoices, contacts, credit notes, bank transactions, and payments, with apaginationobject (page,pageSize,pageCount,itemCount). Invoice and credit note line items come with paging, single reads, andStatusesfilters.summaryOnly=truegives the lightest formunitdp=4keeps four decimal places on unit amounts
Writes
PUT creates and POST creates or updates (matching by ID, or by ContactNumber, InvoiceNumber, or item Code). Bodies may be {"Invoices":[...]}, a bare list, or one object. With the default summarizeErrors=true one bad element rolls back the whole batch and the answer is 400 ValidationException (ErrorNumber: 10) with each failed element and its ValidationErrors. With summarizeErrors=false the good elements are saved and the answer is 200 with StatusAttributeString OK or ERROR on each. Malformed JSON answers 400 PostDataInvalidException, and an unknown ID answers 404 with the plain text The resource you're looking for cannot be found.
Idempotency-Key on a PUT or POST replays the first answer for six minutes. The same key with a different request answers 400 Idempotency Key: KEY is used with a different request., and a key over 128 characters answers Idempotency Key: KEY with length N is too long.
Webhooks
Give an app awebhook: { url, key } in seed config, or POST /_xero/apps/{client_id}/webhook with {"url","key"}. Before the first event the emulator runs intent to receive: one correctly signed and one incorrectly signed empty batch ({"events":[],"firstEventSequence":0,"lastEventSequence":0,"entropy":"..."}). The receiver must answer 200 to the first and 401 to the second; a receiver that does not gets no events until intent to receive passes again (POST /_xero/apps/{client_id}/webhook/intent runs it now and returns the result).
Each delivery is signed with x-xero-signature, the base64 HMAC-SHA256 of the raw body keyed with the webhook key. Changes one API request makes reach each subscribed, connected app as one batch:
CREATE and UPDATE (a payment or allocation updates its invoice). A failed delivery is retried right away and then after 1, 4, and 16 seconds (Xero retries every 15 minutes for a day).
Controls
POST /_xero/apps/{client_id}/webhook— Set app webhook: statusPENDINGPOST /_xero/apps/{client_id}/webhook/intent— Run intent to receivePOST /_xero/tokens— Mint token: optionalclient_id,user(email),organisations(names or tenant IDs), andscopesmints tokens and connections without the browser
Seed config
country_code: NZ, AU, UK, or US: currency, tax rates, timezone
signing_key: { private_jwk } keeps token signatures stable across fresh starts.
Inspector
GET /_inspector shows organisations and users, contacts, invoices and credit notes, payments and bank transactions, apps and connections, and webhook deliveries.
Limits
Payroll, Assets, Projects, Files, Bank Feeds, and Finance APIs; quotes, purchase orders, receipts, expense claims, manual journals, overpayments and prepayments, batch payments, repeating invoices, reports, history and notes, tracking categories, branding themes, currencies other than the base currency, online invoice links, and PDFs.Conformance
See CONFORMANCE.md for every provider.