Xero OAuth refresh token invalid_grant — What it means & how to fix it

How to diagnose and fix Xero refresh token invalid_grant errors

Table of contents

Integrating with Xero's accounting API using OAuth 2.0? Refresh token failures are inevitable. They typically manifest as invalid_grant, halting invoice syncs, transaction imports, or automated accounting workflows.

This guide covers diagnosing and resolving Xero refresh token errors, plus production-tested practices that reduce token lifecycle friction (including how to navigate refresh token race conditions and rotation pitfalls). For broader context, see API Auth Is Deeper Than It Looks and Why OAuth Is Still Hard.

Spot the error

When your backend POSTs to Xero's token endpoint to swap a refresh token for a new access token:

  • Token endpoint: https://identity.xero.com/connect/token
  • Common HTTP status: 400
  • Common OAuth error: invalid_grant

Example response payload:

Xero OAuth error response

📋

{
  "error": "invalid_grant",
  "error_description": "Token has been expired or revoked."
}
  

Xero may also include an error_description field with additional context. Either way, the meaning is consistent: the refresh token you have stored can't be used to mint a new access token. Retrying the same request will keep failing until you address the underlying cause.

Why did Xero reject the refresh token?

Xero returns invalid_grant when the refresh token you're using can't be exchanged for a new access token. This happens because the token is invalid, expired, revoked, or you're using a stale token after rotation. The key is to treat invalid_grant as a token lifecycle problem, not as a transient network issue.

There are several recurring root causes:

Most common: You're not using the latest refresh token

Some OAuth providers rotate refresh tokens. Xero does: when a refresh token is exchanged, the previous access and refresh tokens are invalidated and new tokens are returned. If you keep refreshing with an older token, your next refresh can fail with invalid_grant.

This is also the most common failure mode when teams run token refreshes in multiple processes/containers without proper locking: one process stores the "new" refresh token, another process overwrites it with the stale one, and suddenly every refresh starts failing.

Refresh token expired due to inactivity

Xero refresh tokens expire after 60 days of inactivity. If a refresh token isn't used within that window, the authorization is effectively revoked and the user must re-authorize. This shows up as invalid_grant on refresh.

Access token expired before refresh

Xero access tokens expire in 30 minutes. If you miss the refresh window or the access token expires before you refresh, you'll need to use the refresh token. However, if the refresh token itself has expired due to inactivity, Xero returns invalid_grant and requires re-authorization.

User revoked access or tenant disconnected

If an end-user removes your application from their Xero organization or revokes access in their account settings, Xero immediately invalidates all associated tokens. This can happen through:

  • Removing the OAuth app from Xero organization settings
  • Manually revoking access in account permissions
  • Organization admin disconnecting the app
  • Tenant becoming inactive (trial ended, organization deleted, or demo reset)

Refresh request rejected → token revoked

Xero may revoke refresh tokens when a refresh request is rejected (for example, when the server returns 400 or 401). Common reasons:

  • The refresh token is stale because it was already rotated
  • The refresh token was retried after the grace period for duplicate refreshes
  • The request used wrong client credentials (often after a client secret rotation)

Scope changes or app configuration updates

If the OAuth app's scopes are modified after the initial authorization, or if the app configuration changes in Xero, existing refresh tokens may become invalid and require re-authorization.

Client credentials mismatch

Using incorrect client_id or client_secret when refreshing tokens will result in invalid_grant errors. This often happens after:

  • Client secret rotation
  • Using credentials from the wrong Xero app
  • Copy-paste errors in environment variables

Refresh-token concurrency bugs (race conditions)

Refresh tokens become tricky at scale because token refresh is triggered by many events:

  • Scheduled accounting data syncs
  • Webhook deliveries
  • User-triggered "sync now" actions
  • Background retries after 401s

If two workers refresh the same Xero connection at the same time, you can get a race:

  • Worker A refreshes and receives a new refresh token
  • Worker B refreshes with the now-stale token and gets invalid_grant (or worse: succeeds later and overwrites state)

This class of bugs is subtle, sporadic, and highly load-dependent. If this sounds familiar, see our deep dive on OAuth token refresh concurrency.

Security heuristics and automatic revocations

Xero may revoke refresh tokens based on security heuristics or policy changes. This can include:

  • Unusual access patterns detected by Xero's systems
  • Suspicious activity or potential security threats
  • Rate limit violations or abuse detection
  • Token exposure or compromise detected

How to fix it

1. Confirm you're using the latest refresh token

If Xero returns a new refresh_token on refresh, always persist it and use that value for the next refresh. Xero invalidates previous refresh tokens as soon as a new one is issued.

Fix:

  • Store the latest refresh_token returned by Xero every time you refresh
  • Update the stored refresh token immediately after each refresh (see concurrency section below)

2. Verify your refresh request is correct

Double-check the basics:

  • grant_type=refresh_token
  • Content-Type is application/x-www-form-urlencoded
  • Authorization uses Basic Auth (client_id:client_secret base64-encoded) or includes credentials in the request body
  • You're hitting the correct token endpoint: https://identity.xero.com/connect/token

3. Check if user revoked access

The most common cause is that the user manually revoked access or disconnected the app. There's no way to revive a revoked token.

4. Eliminate refresh concurrency (single-flight + locking)

If you run multiple workers, you need to treat "refresh token per connection" as a shared resource.

At minimum, ensure:

  • Only one refresh can run for a given connection at a time (distributed lock or single-flight)
  • Other requests wait for the in-flight refresh and then use the newly stored access token
  • Updates to (access_token, refresh_token, expires_at) happen atomically

Rather not build this yourself? Nango (open-source OAuth) includes refresh concurrency controls. Your app just needs to fetch the latest access token before each Xero API call.

5. If it's truly invalid/expired/revoked: trigger re-auth (don't keep retrying)

After you've ruled out rotation and credential issues, treat invalid_grant as a terminal state for that connection.

Practical production playbook:

  • Retry once to cover rare partial failures (Xero allows a 30-minute grace period to retry with existing token)
  • If it fails again, mark the connection as "needs re-auth"
  • Pause background syncs for that connection
  • Ask the user to reconnect Xero in-product

This keeps retries quiet, avoids wasteful refresh loops, and gives users a clear next step.

How to prevent Xero refresh token issues

Use this checklist to reduce "why did my Xero integration break?" tickets:

  • Refresh proactively before expiration
    Xero access tokens expire in 30 minutes. Refresh tokens before they expire to keep connections active and avoid unexpected failures.
  • Persist the rotated refresh token
    Treat "save the new refresh token" as mandatory after every refresh. Xero rotates refresh tokens, and the old one becomes unusable immediately.
  • Discard old access tokens immediately
    Cache invalidation mistakes are a frequent cause of hard-to-debug auth failures. Once you refresh, discard the old Xero access token.
  • Make refresh single-flight and atomic
    Ensure one refresh per connection and atomic writes of (access_token, refresh_token, expires_at). If you're unsure, read How to handle concurrency with OAuth token refreshes.
  • Watch invalid_grant trends
    A baseline rate is normal at scale, but spikes usually indicate rotation, concurrency, or credential issues.
  • Handle app disconnection gracefully
    Set up monitoring to detect when your OAuth app is removed from Xero organizations so you can clean up associated data and notify users proactively.
  • Invest in a clean re-auth UX
    A clear "Reconnect Xero" flow saves days of back-and-forth with customers and reduces support burden significantly.

Skip the headache, let Nango refresh for you

Nango's open-source API auth offers:

  • 600+ pre-built OAuth flows, including full support for all Xero APIs
  • Automatic OAuth access token refreshing and rotation
  • Webhooks when a refresh token is revoked, so you can warn the user instantly
  • Built-in error handling for all OAuth edge cases

If you're building a Xero API integration and you're tired of token lifecycle edge cases, Nango can run the refresh pipeline for you.

Focus on product features and let Nango handle the token lifecycle.

Oliver Anyanwu
Developer Relations

Stay in the loop

Bi-weekly tips, learnings & guides for product integrations

Join 5,000+ engineers, eng leaders & product managers
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.