Summary and recommendation
Xero's Accounting API exposes a read-only GET /Users endpoint under the base URL https://api.xero.com/api.xro/2.0.
Authentication uses OAuth 2.0 Authorization Code flow with PKCE;
the accounting.settings.read scope is sufficient for read access to user records.
Every request must include a Xero-Tenant-Id header populated from a prior GET https://api.xero.com/connections call - requests without this header will fail.
Access tokens expire after 30 minutes;
the offline_access scope must be requested to receive a refresh token for long-lived integrations.
Rate limits are enforced at 60 calls per minute and 5,000 calls per day per app per tenant;
HTTP 429 is returned on breach, and the X-Rate-Limit-Problem header distinguishes whether the minute or day limit was hit.
API quick reference
| Has user API | Yes |
| Auth method | OAuth 2.0 (Authorization Code flow with PKCE supported) |
| Base URL | Official docs |
| SCIM available | No |
Authentication
Auth method: OAuth 2.0 (Authorization Code flow with PKCE supported)
Setup steps
- Register an application at developer.xero.com/myapps to obtain a Client ID and Client Secret.
- Configure redirect URIs and select required OAuth 2.0 scopes in the app settings.
- Redirect the user to https://login.xero.com/identity/connect/authorize with response_type=code, client_id, redirect_uri, scope, and state parameters.
- Exchange the returned authorization code for an access token via POST to https://identity.xero.com/connect/token.
- Include the access token as a Bearer token in the Authorization header on all API requests.
- Use the refresh token to obtain new access tokens before expiry (access tokens expire after 30 minutes).
- For multi-org apps, retrieve the list of connected tenants via GET https://api.xero.com/connections and pass the target Xero-Tenant-Id header on each request.
Required scopes
| Scope | Description | Required for |
|---|---|---|
| openid | Required for OpenID Connect authentication; returns an ID token. | Authentication |
| profile | Returns basic profile information about the authenticated user. | Identifying the authenticated user |
| Returns the email address of the authenticated user. | Identifying the authenticated user | |
| accounting.settings | Read/write access to organisation settings. | GET /Users (read organisation users) |
| accounting.settings.read | Read-only access to organisation settings including users. | GET /Users (read-only) |
| offline_access | Issues a refresh token to allow long-lived API access without re-authentication. | Maintaining persistent API sessions |
User object / data model
| Field | Type | Description | On create | On update | Notes |
|---|---|---|---|---|---|
| UserID | string (GUID) | Unique Xero identifier for the user. | not applicable | not applicable | Read-only; assigned by Xero. |
| EmailAddress | string | Email address of the user. | not applicable | not applicable | Read-only via API. |
| FirstName | string | First name of the user. | not applicable | not applicable | Read-only via API. |
| LastName | string | Last name of the user. | not applicable | not applicable | Read-only via API. |
| UpdatedDateUTC | string (datetime) | UTC timestamp of the last update to the user record. | not applicable | not applicable | Read-only; Xero-managed. |
| IsSubscriber | boolean | Indicates whether the user is the Xero subscriber (account owner). | not applicable | not applicable | Read-only. |
| OrganisationRole | string (enum) | Role of the user within the organisation. Values: READONLY, INVOICEONLY, STANDARD, ADVISER, MANAGEDCLIENT, CASHBOOKCLIENT. | not applicable | not applicable | Read-only via API; role changes must be made in the Xero UI. |
Core endpoints
List all users in an organisation
- Method: GET
- URL:
https://api.xero.com/api.xro/2.0/Users - Watch out for: This endpoint is read-only. You cannot create, update, or delete users via the Accounting API. User management must be performed through the Xero web UI.
Request example
GET /api.xro/2.0/Users HTTP/1.1
Host: api.xero.com
Authorization: Bearer {access_token}
Xero-Tenant-Id: {tenant_id}
Accept: application/json
Response example
{
"Users": [
{
"UserID": "d1164823-0ac1-41ad-987b-b4e30be0b9d0",
"EmailAddress": "jane@example.com",
"FirstName": "Jane",
"LastName": "Smith",
"UpdatedDateUTC": "/Date(1619000000000+0000)/",
"IsSubscriber": true,
"OrganisationRole": "ADVISER"
}
]
}
Get a single user by UserID
- Method: GET
- URL:
https://api.xero.com/api.xro/2.0/Users/{UserID} - Watch out for: Returns a single-element Users array even for a specific UserID lookup.
Request example
GET /api.xro/2.0/Users/d1164823-0ac1-41ad-987b-b4e30be0b9d0 HTTP/1.1
Host: api.xero.com
Authorization: Bearer {access_token}
Xero-Tenant-Id: {tenant_id}
Accept: application/json
Response example
{
"Users": [
{
"UserID": "d1164823-0ac1-41ad-987b-b4e30be0b9d0",
"EmailAddress": "jane@example.com",
"FirstName": "Jane",
"LastName": "Smith",
"IsSubscriber": true,
"OrganisationRole": "ADVISER"
}
]
}
Get authenticated user identity (OpenID Connect)
- Method: GET
- URL:
https://api.xero.com/api.xro/2.0/Users (authenticated user context via /identity/connect/userinfo) - Watch out for: This is the OIDC userinfo endpoint, not the Accounting API Users endpoint. Requires openid + profile + email scopes.
Request example
GET /identity/connect/userinfo HTTP/1.1
Host: api.xero.com
Authorization: Bearer {access_token}
Response example
{
"sub": "abcd1234-...",
"email": "user@example.com",
"given_name": "Jane",
"family_name": "Smith",
"xero_userid": "d1164823-..."
}
List connected tenants (organisations)
- Method: GET
- URL:
https://api.xero.com/connections - Watch out for: The tenantId from this response must be passed as the Xero-Tenant-Id header on all Accounting API calls.
Request example
GET /connections HTTP/1.1
Host: api.xero.com
Authorization: Bearer {access_token}
Response example
[
{
"id": "7369d8a3-...",
"tenantId": "b2c3d4e5-...",
"tenantType": "ORGANISATION",
"tenantName": "Demo Company (Global)",
"createdDateUtc": "2023-01-15T10:00:00"
}
]
Rate limits, pagination, and events
- Rate limits: Xero enforces per-app, per-tenant minute-level and daily limits on the Accounting API.
- Rate-limit headers: Yes
- Retry-After header: No
- Rate-limit notes: When the per-minute limit is exceeded, Xero returns HTTP 429. The response includes an X-Rate-Limit-Problem header indicating whether the minute or day limit was hit. The X-MinLimit-Remaining and X-DayLimit-Remaining headers report remaining quota. Xero docs do not document a Retry-After header.
- Pagination method: offset
- Default page size: 100
- Max page size: 100
- Pagination pointer: page
| Plan | Limit | Concurrent |
|---|---|---|
| All apps (standard) | 60 API calls per minute per app per tenant; 5,000 API calls per day per app per tenant | 0 |
- Webhooks available: Yes
- Webhook notes: Xero supports webhooks for a defined set of Accounting API object events. Webhooks are configured per-app in the Xero developer portal. Xero sends a signed POST payload to the registered endpoint; the receiver must respond with HTTP 200 within 5 seconds. Xero uses an intent-to-receive verification handshake on setup.
- Alternative event strategy: User-specific events (user added, user role changed) are not available as webhook events. Polling GET /Users with the If-Modified-Since header is the only programmatic way to detect user changes.
- Webhook events: Account.Created, Account.Updated, Contact.Created, Contact.Updated, Invoice.Created, Invoice.Updated, CreditNote.Created, CreditNote.Updated, PurchaseOrder.Created, PurchaseOrder.Updated
SCIM API status
- SCIM available: No
- SCIM version: Not documented
- Plan required: Not documented
- Endpoint: Not documented
Limitations:
- Xero does not offer a native SCIM 2.0 endpoint on any plan.
- No native SAML SSO is available; SSO is only available via third-party identity providers (e.g., miniOrange, AuthDigital) acting as middleware.
- User provisioning and deprovisioning cannot be automated via any official Xero API; it must be performed through the Xero web UI.
- Feature requests for SCIM and native SSO have been open in the Xero community for many years without an official release date.
Common scenarios
The primary automation scenario is building an identity graph of Xero users and their roles across one or more tenants.
Call GET /connections to enumerate tenants, then GET /Users?page=N per tenant - 100 users per page, offset pagination - to retrieve UserID, EmailAddress, FirstName, LastName, OrganisationRole, IsSubscriber, and UpdatedDateUTC.
Store UpdatedDateUTC per user record and pass it as the If-Modified-Since header on subsequent polling runs to retrieve only changed records;
this is the only available mechanism for detecting user lifecycle changes, as Xero's webhook event catalog does not include user-scoped events such as add, remove, or role change.
To resolve the identity of the OAuth-authenticated user, call GET https://api.xero.com/identity/connect/userinfo with openid, profile, and email scopes;
the xero_userid field in that response maps directly to UserID in the Accounting API, but OrganisationRole must be fetched per tenant separately since a user may hold different roles across organisations.
Audit all users and roles in a Xero organisation
- Complete OAuth 2.0 Authorization Code flow with scopes: openid profile email accounting.settings.read offline_access.
- Call GET https://api.xero.com/connections to retrieve the tenantId for the target organisation.
- Call GET https://api.xero.com/api.xro/2.0/Users with Xero-Tenant-Id header set.
- Iterate the returned Users array; record UserID, EmailAddress, FirstName, LastName, OrganisationRole, and IsSubscriber.
- Store UpdatedDateUTC per user to enable incremental polling using the If-Modified-Since header on subsequent runs.
Watch out for: Results are limited to 100 users per page. Use the page query parameter (?page=2, etc.) to retrieve additional pages if the organisation has more than 100 users.
Detect user changes via polling
- On first run, call GET /Users and record the latest UpdatedDateUTC across all returned users.
- On subsequent runs, call GET /Users with header If-Modified-Since: {last_recorded_datetime} in RFC 1123 format.
- Compare the returned user list against the previously stored snapshot to identify added, removed, or role-changed users.
- Alert or sync downstream systems based on detected differences.
Watch out for: Xero does not provide webhook events for user changes, so polling is the only available mechanism. The If-Modified-Since filter applies to the UpdatedDateUTC field on user records.
Identify the authenticated user after OAuth login
- Complete OAuth 2.0 flow with scopes: openid profile email.
- Call GET https://api.xero.com/identity/connect/userinfo with the Bearer access token.
- Extract sub (Xero user GUID), email, given_name, family_name, and xero_userid from the response.
- Optionally call GET /connections to list all organisations the authenticated user has authorised, then GET /Users/{UserID} per tenant to retrieve their OrganisationRole in each.
Watch out for: The xero_userid in the OIDC userinfo response corresponds to the UserID field in the Accounting API /Users endpoint, but the user may have different OrganisationRole values across different tenants.
Why building this yourself is a trap
The critical caveat for any integration targeting Xero user management is that the Accounting API /Users endpoint is strictly read-only - there is no API method to invite, provision, update roles for, or remove users from a Xero organisation on any current plan tier.
Xero also offers no native SCIM 2.0 endpoint and no native SAML SSO; the OrganisationRole field returned by the API cannot be written back via any official endpoint. Any workflow that requires write operations against Xero's user model must route through the web UI at Settings > Users or through third-party middleware.
Integrations that assume bidirectional user lifecycle control based on the presence of a /Users endpoint will hit this wall immediately.
Automate Xero workflows without one-off scripts
Stitchflow builds and maintains end-to-end IT automation across your SaaS stack, including apps without APIs. Built for exactly how your company works, with human approvals where they matter.