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

How to diagnose and fix LinkedIn refresh token invalid_grant errors

Table of contents

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

If you integrate with LinkedIn using OAuth 2.0, you’ll eventually run into a failed token refresh. It often shows up as invalid_grant, and it can break scheduled syncs, background jobs, or user-triggered actions.

This article shows you how to diagnose and resolve LinkedIn refresh token errors, and shares the engineering practices we’ve seen work best in production. 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 https://www.linkedin.com/oauth/v2/accessToken to swap a refresh token for a new access token, LinkedIn can answer with HTTP error code 400:

LinkedIn OAuth refresh error

📋

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

That single JSON payload means the refresh token is unusable. Retrying the same request will keep failing until you address the underlying cause.

If you want a comparable recovery flow for another provider, see HubSpot OAuth BAD_REFRESH_TOKEN.

Why was the refresh token revoked?

There are a few recurring root causes. The key is to treat invalid_grant as a token lifecycle problem, not as a transient network problem.

Refresh token expired (fixed lifetime)

LinkedIn refresh tokens expire after a fixed one-year window. The countdown starts at issuance and does not repeat when you refresh. Even frequent refreshes won’t extend the one-year deadline.

User or admin revoked your app

If the member revokes your app in LinkedIn settings, or an org admin removes access, all tokens tied to that grant are immediately invalid.

App removed or permissions changed

Changing your app’s permissions or scopes can invalidate existing refresh tokens and require a new authorization.

LinkedIn revoked the token for policy or security reasons

LinkedIn reserves the right to revoke tokens for policy, technical, or security reasons, which can invalidate refresh tokens even before expiration.

Refresh-token concurrency bugs (race conditions)

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

  • scheduled syncs
  • webhooks
  • user-triggered “sync now”
  • background retries after 401s

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

  • Worker A refreshes and writes new tokens
  • Worker B refreshes with stale state and overwrites the latest values

This class of bugs is subtle, sporadic, and highly load-dependent. If this sounds familiar, see How to handle concurrency with OAuth token refreshes.

How to fix it

1. Confirm the token age and re-auth if expired

LinkedIn refresh tokens have a fixed one-year lifetime. If the token is near or past that window, re-auth is required.

Fix:

  • Track refresh token issuance time.
  • Prompt re-auth before the one-year deadline.

2. Verify your refresh request is correct

Double-check the basics:

  • grant_type=refresh_token
  • Content-Type is application/x-www-form-urlencoded
  • You’re using the correct client credentials
  • You’re hitting the correct token endpoint: https://www.linkedin.com/oauth/v2/accessToken

3. 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

If you don’t want to build this yourself, Nango handles refresh concurrency for you. Just make sure your app fetches the latest access token before each API request.

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

Once you’ve confirmed it’s not a request or concurrency bug, treat invalid_grant as a permanent failure for that connection.

What works well in production:

  • Retry once (to cover rare partial failures)
  • If it fails again, mark the connection as re-auth required
  • Stop background syncs for that connection
  • Prompt the user in-product to reconnect LinkedIn

How to prevent refresh token issues

These practices dramatically reduce “why did my LinkedIn integration break?” tickets:

  • Track token issuance time
    LinkedIn refresh tokens have a fixed one-year lifetime. Alert before the deadline and re-auth early.
  • Persist tokens atomically
    Always write updated tokens after each refresh to avoid regressions.
  • Make refresh logic concurrency-safe
    Implement single-flight per connection and atomic token writes.
  • Monitor invalid_grant rates
    A small background rate is normal at scale (users revoke access, accounts change). Spikes are a signal to investigate quickly.
  • Design a great re-auth UX
    Have a first-class “Reconnect LinkedIn” flow. Most teams lose days of engineering time because re-auth is an afterthought.

Skip the headache, let Nango refresh for you

Nango is an open-source auth layer that handles OAuth token lifecycle management in production:

  • Secure storage for access + refresh tokens
  • Automatic access-token refresh
  • Safe handling of refreshed tokens
  • Concurrency-safe refresh logic (no race conditions across workers)
  • Clear signals when a connection needs re-authentication (so you can prompt users quickly)

If you’re building a LinkedIn integration (or any integration) and you’re tired of token lifecycle edge cases, Nango handles the token lifecycle for you.

See the setup details and OAuth URLs in the LinkedIn API integration Nango Docs.

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.