Jira OAuth refresh token invalid_grant — What it means & how to fix it
How to diagnose and fix Jira refresh token invalid_grant errors
If you integrate with Jira using OAuth 2.0 (3LO), a token refresh will eventually fail. Symptom: your refresh call returns invalid_grant. Impact: scheduled syncs stop, issue writes fail, and your integration effectively disconnects until the token lifecycle is repaired.
This article explains how to spot Jira refresh-token failures, why they happen in production (rotating refresh tokens + concurrency), and what to change so they don’t keep recurring. For broader context, see API Auth Is Deeper Than It Looks and Why is OAuth still hard in 2025?.
Spot the error
Even if you call it “Jira OAuth”, the OAuth authorization server is Atlassian. Token refresh happens against auth.atlassian.com, and Jira API calls go via api.atlassian.com (not {your-site}.atlassian.net).
When your backend refreshes tokens:
- Token endpoint:
https://auth.atlassian.com/oauth/token - Often HTTP status:
403 - OAuth error:
invalid_grant
Example response payload (shape):
Atlassian OAuth refresh error response (Jira)
📋
{
"error": "invalid_grant",
"error_description": "Token has been expired or revoked."
}
Atlassian also documents the error description: “Unknown or invalid refresh token.” Either way, the meaning is consistent: the refresh token you’re sending can’t be exchanged for a new access token. Retrying the same refresh token won’t help.
Why did Jira/Atlassian reject the refresh token?
In Jira Cloud OAuth (3LO), invalid_grant is almost always a token lifecycle problem. The root cause is typically one of: rotating refresh tokens not being handled correctly, inactivity expiry, or the user’s authorization changing.
Most common: you’re not using the newest refresh token (rotating refresh tokens)
Atlassian uses rotating refresh tokens: each successful refresh returns a new refresh token and disables the refresh token used in that request. If you keep using the older refresh token, your next refresh fails.
This frequently happens when:
- Your code stores the access token but fails to persist the new refresh token
- A retry replays the same refresh token after a timeout
- Multiple workers refresh the same connection without coordination
Refresh concurrency bugs (race conditions)
If two workers refresh the same Jira connection at around the same time:
- Worker A refreshes and stores the new refresh token
- Worker B refreshes with the now-disabled refresh token
- Result:
invalid_grant(and sometimes state corruption if writes aren’t atomic)
If you want the deeper implementation patterns, see How to handle concurrency with OAuth token refreshes.
The refresh token expired due to inactivity
Atlassian rotating refresh tokens have an inactivity expiry window (commonly 90 days by default). If the integration sits idle beyond that period, the refresh token expires and the user must re-authorize.
The user revoked consent (grant revoked)
If the user revokes access to your 3LO app, refresh tokens stop working. The only fix is to send the user through the authorization flow again.
The user’s Atlassian account password was changed
Atlassian explicitly lists password changes as a reason the refresh-token exchange can return invalid_grant.
Client credentials mismatch / wrong 3LO app
Refreshing with the wrong client_id/client_secret (wrong environment, rotated secret not deployed, wrong app) can also lead to refresh failures.
How to fix it
1) Persist the rotated refresh token (every refresh)
After every successful refresh, overwrite the stored refresh token with the new refresh token Atlassian returned.
2) Make refresh single-flight per connection
Only one refresh in flight per connection. Everyone else should wait and reuse the updated token set. Persist (access_token, refresh_token, expires_at) together.
3) Verify you’re using Atlassian’s OAuth endpoints (Jira uses Atlassian OAuth)
Core endpoints:
- Authorize:
https://auth.atlassian.com/authorize(withaudience=api.atlassian.comandoffline_access) - Token + refresh:
https://auth.atlassian.com/oauth/token
Jira request routing:
- Get
cloudid:https://api.atlassian.com/oauth/token/accessible-resources - Call Jira APIs:
https://api.atlassian.com/ex/jira/{cloudid}/{api}
If you’re still calling {site}.atlassian.net/rest/... with a 3LO access token, you’re using the wrong base URL for OAuth 2.0 (3LO).
If you’re debugging callback URLs locally, see 3 easy ways to do OAuth redirects on localhost (with HTTPS).
4) If it’s terminal: re-auth (don’t retry forever)
After you’ve ruled out stale refresh tokens, concurrency, and credentials:
- Mark the connection as “needs re-auth”
- Pause background syncs
- Send the user through the authorization flow again
How to prevent Jira refresh token issues
Use this checklist to reduce “why did my Jira API integration break?” tickets:
- Always store the newest refresh token
Rotating refresh tokens mean old refresh tokens become invalid. - Single-flight refresh + atomic writes
Lock refresh per connection and persist the full token set together. - Plan for inactivity expiry
If a customer doesn’t use your Jira integration for long periods, expect a reconnect. - Clear reconnect UX
A fast “Reconnect Jira” flow reduces support load and avoids retry storms.
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 Jira 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 Jira API integration and you don’t want to maintain Atlassian OAuth edge cases (rotating refresh tokens, cloudid routing, and revocation handling), Nango can run the refresh pipeline for you.
Focus on product features and let Nango handle the token lifecycle.




