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

How to diagnose and fix Stripe refresh token invalid_grant errors

Table of contents

Integrating with Stripe's API using OAuth 2.0? Refresh token failures are inevitable. They typically surface as invalid_grant, disrupting payment processing, webhook handling, or account synchronization.

This guide covers diagnosing and resolving Stripe 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 Stripe's token endpoint to swap a refresh token for a new access token:

  • Token endpoint: https://api.stripe.com/v1/oauth/token (Stripe Apps) or https://connect.stripe.com/oauth/token (Connect)
  • Common HTTP status: 400
  • Common OAuth error: invalid_grant

Example response payload:

Stripe OAuth error response

📋

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

Stripe 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 Stripe reject the refresh token?

Stripe 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. Stripe does: when a refresh token is exchanged, the previous refresh token is invalidated and a new refresh token is returned (rolled). 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.

Authorization code reused (Stripe-specific)

Stripe has a strict security policy: if an authorization code is used more than once, Stripe invalidates the entire authorization, including all associated refresh tokens. This shows up as invalid_grant with the error description "This authorization code has already been used."

This commonly happens when:

  • A user refreshes the browser page after OAuth redirect
  • Your callback handler processes the same authorization code twice
  • Network retries cause duplicate token exchange requests

Refresh token expired due to inactivity

Stripe refresh tokens expire after 1 year for Stripe Apps OAuth. 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

Stripe access tokens expire in 1 hour. 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, Stripe returns invalid_grant and requires re-authorization.

Account disconnected or access revoked

If a user disconnects their Stripe account from your platform or revokes access in their account settings, Stripe immediately invalidates all associated tokens. This can happen through:

  • Disconnecting the account from your platform
  • Manually revoking access in Stripe account settings
  • Account deauthorization via API

Refresh request rejected → token revoked

Stripe 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 rolled
  • The refresh token was retried after expiration
  • The request used wrong client credentials (often after a client secret rotation)

API key mode mismatch

Stripe requires that the API key mode (live or test) matches the mode of the refresh token. Using a test mode API key with a live mode refresh token (or vice versa) will result in invalid_grant errors.

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 Stripe, 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 Stripe 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 payment syncs
  • Webhook deliveries
  • User-triggered "sync now" actions
  • Background retries after 401s

If two workers refresh the same Stripe 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

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

  • Unusual access patterns detected by Stripe'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 Stripe returns a new refresh_token on refresh, always persist it and use that value for the next refresh. Stripe invalidates previous refresh tokens as soon as a new one is issued (rolled).

Fix:

  • Store the latest refresh_token returned by Stripe 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 your secret API key
  • You're hitting the correct token endpoint: https://api.stripe.com/v1/oauth/token (Stripe Apps) or https://connect.stripe.com/oauth/token (Connect)
  • API key mode (live/test) matches the refresh token mode

3. Check if account disconnected

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

4. Prevent authorization code reuse

Ensure your OAuth callback handler only processes each authorization code once. Implement idempotency checks to prevent duplicate processing:

  • Store processed authorization codes temporarily
  • Check if a code has already been used before exchanging it
  • Handle page refreshes gracefully without re-processing the same code

5. 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 Stripe API call.

6. 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
  • If it fails again, mark the connection as "needs re-auth"
  • Pause background syncs for that connection
  • Ask the user to reconnect Stripe in-product

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

How to prevent Stripe refresh token issues

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

  • Refresh proactively before expiration
    Stripe access tokens expire in 1 hour. Refresh tokens before they expire to keep connections active and avoid unexpected failures.
  • Persist the rolled refresh token
    Treat "save the new refresh token" as mandatory after every refresh. Stripe rolls 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 Stripe 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 account disconnection gracefully
    Set up monitoring to detect when accounts are disconnected from your platform so you can clean up associated data and notify users proactively.
  • Invest in a clean re-auth UX
    A clear "Reconnect Stripe" flow saves days of back-and-forth with users 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 Stripe 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 Stripe 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.