LinkedIn API Error 401: Invalid Grant & How to Fix Token Expiration

We know the drill. You’ve successfully implemented LinkedIn’s authentication, your app has been happily publishing posts for weeks, and then suddenly the integration flatlines. Your logs are flooded with HTTP 401 Unauthorized responses and cryptic invalid_grant JSON payloads.

You haven’t been hacked, and the user likely didn’t revoke your app’s permissions. You’ve simply crashed into the unforgiving wall of LinkedIn’s token Expiration policies.

The “Why” Behind the Invalid Grant

When you initially authenticate a user via the 3-legged Auth Code Flow, LinkedIn grants you an access token. Unlike some platforms that issue long-lived or permanent tokens, LinkedIn’s standard access tokens die after exactly 60 days.

If your application is approved for programmatic refresh Scopes (often requiring specific developer platform access), you will also receive a 365-day LinkedIn refresh token. When that 60-day window closes, your primary access token becomes completely useless, and any subsequent API call will throw a LinkedIn OAuth error.

To restore service, you must hit the v2/accessToken endpoint and exchange your unexpired refresh token for a fresh 60-day access token before it fails in production. If you fail to do this, or if your refresh token hits its 365-day hard limit, the user must physically log in and re-authorize your app.

The Manual Fix: Rotating the Refresh Token

To handle this natively, you need to build a worker that monitors token lifecycles and safely executes the refresh exchange. Note that LinkedIn strictly requires application/x-www-form-urlencoded payloads for this specific endpoint.

Here is a Node.js snippet using axios to successfully rotate the token:

JavaScript

const axios = require("axios");

const CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;
const CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;

async function refreshLinkedInToken(storedRefreshToken) {
  try {
    const response = await axios.post(
      "https://www.linkedin.com/oauth/v2/accessToken",
      new URLSearchParams({
        grant_type: "refresh_token",
        refresh_token: storedRefreshToken,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET
      }).toString(),
      {
        headers: {
          "Content-Type": "application/x-www-form-urlencoded"
        }
      }
    );

    const newAccessToken = response.data.access_token;
    // Sometimes LinkedIn returns a new refresh token; otherwise keep the old one
    const newRefreshToken = response.data.refresh_token || storedRefreshToken;
    const expiresInSeconds = response.data.expires_in;

    // Persist the fresh tokens and exact expiration to your database
    // await updateDatabaseAuth(newAccessToken, newRefreshToken, expiresInSeconds);

    return newAccessToken;
  } catch (error) {
    console.error("LinkedIn OAuth error: Token refresh failed", error.response?.data || error.message);
    throw new Error("invalid_grant: Refresh token expired or revoked. Prompt user to re-authenticate.");
  }
}

The Pivot: Stop Babysitting OAuth 2.0 States

Building distributed token refresh workers, tracking 60-day countdown timers, and handling concurrent refresh race conditions is a massive engineering distraction. You are trying to ship product features, not build a bulletproof OAuth 2.0 state machine just to keep a single social integration alive.

This is exactly why our team at Ayrshare built our platform. Think of us as your ultimate API insurance policy. When you authenticate your users through Ayrshare, we completely abstract the token management layer. We securely monitor the Expiration countdowns on our end, automatically perform the refresh exchanges in the background, and seamlessly cycle in fresh tokens. You never have to write a CRON job for social media tokens again.

The Comparison: Native vs. Ayrshare

Here is how your publishing logic transforms when you stop managing token databases and offload the complexity to us.

Before (Native LinkedIn API)

JavaScript

// You must constantly query your DB to check if the 60-day token is dead
const auth = await getUserAuth(userId);

if (Date.now() >= auth.expiresAt) {
  // Execute manual refresh logic and handle potential race conditions
  auth.accessToken = await refreshLinkedInToken(auth.refreshToken);
}

// Don't forget LinkedIn's strict versioning headers for the v2 API!
const response = await axios.post(
  "https://api.linkedin.com/v2/ugcPosts",
  { /* Complex LinkedIn Post Payload */ },
  { 
    headers: { 
      "Authorization": `Bearer ${auth.accessToken}`, 
      "X-Restli-Protocol-Version": "2.0.0" 
    } 
  }
);

After (Ayrshare API)

JavaScript

// We manage the token expiration, background refreshes, and API versioning.
const ayrshare = require("ayrshare")("YOUR_AYRSHARE_API_KEY");

const response = await ayrshare.post({
  post: "Just shipped our new feature! 🚀",
  platforms: ["linkedin"],
  profileKeys: ["client_profile_key"] // Sandboxes the user, we handle the auth state
});

// Done. No cron jobs, no headers to memorize, no invalid_grant errors.

How to Refresh LinkedIn API Access Token

This video visually demonstrates the token exchange process and provides additional context on manually managing LinkedIn’s OAuth credentials.