DEV Community

Preecha
Preecha

Posted on

How to Use LinkedIn API: Complete Professional Network Integration Guide (2026)

TL;DR

The LinkedIn API lets developers integrate with LinkedIn’s professional network using OAuth 2.0, RESTful, and GraphQL endpoints. You can work with profiles, posts, comments, company pages, ads, and more, subject to product access and rate limits of roughly 100–500 requests per day per app.

Try Apidog today

This guide shows how to configure OAuth, retrieve profile data, publish content, manage company pages, handle rate limits, and prepare a LinkedIn integration for production.

Introduction

LinkedIn has more than 900 million professional users across 200+ countries. If you are building a recruiting tool, marketing platform, or B2B application, the LinkedIn API can automate workflows that would otherwise require manual work.

Typical automation targets include:

  • Publishing content on a schedule
  • Capturing and processing leads
  • Tracking engagement
  • Managing company-page updates
  • Supporting recruitment workflows

Apidog can help you test LinkedIn endpoints, validate OAuth flows, inspect responses, and debug permission problems in a shared API workspace.

What Is the LinkedIn API?

LinkedIn provides RESTful and GraphQL APIs for accessing professional network data. Available capabilities depend on the API product and your app’s approval status.

The API can support:

  • User profile information, with consent
  • Company pages and updates
  • Posts, comments, and reactions
  • Connections, with limited access
  • Job postings and applications
  • LinkedIn Ads management
  • Lead generation forms
  • Messaging for limited partners

Key Features

Feature Description
RESTful + GraphQL Multiple API styles
OAuth 2.0 User authorization is required
Rate limiting Approximately 100–500 requests/day per app
Company pages CRUD operations
Ads API Campaign management
Webhooks Real-time notifications
Media upload Images and videos

API Products

API Access level Use case
Sign In with LinkedIn Open User authentication
Profile API Partner Read user profiles
Company Admin API Partner Manage company pages
Ads API Partner Manage ad campaigns
Job Posting API Partner Post and manage jobs
Marketing Developer Platform Partner Broader API access

API Versions

Version Status End date
v2 Current Active
v1 Retired December 2023

Getting Started: Authentication Setup

Step 1: Create a LinkedIn Developer App

Before calling the API:

  1. Visit the LinkedIn Developer Portal.
  2. Sign in with your LinkedIn account.
  3. Open My Apps.
  4. Select Create App.
  5. Enter the app name, logo, description, and required organization details.

Step 2: Configure App Credentials

Store your app credentials in environment variables rather than hard-coding them.

const LINKEDIN_CLIENT_ID = process.env.LINKEDIN_CLIENT_ID;
const LINKEDIN_CLIENT_SECRET = process.env.LINKEDIN_CLIENT_SECRET;
const LINKEDIN_REDIRECT_URI = process.env.LINKEDIN_REDIRECT_URI;

// Get these from your app dashboard:
// https://www.linkedin.com/developers/apps/{appId}/auth
Enter fullscreen mode Exit fullscreen mode

Your redirect URI must match the URI configured in the LinkedIn app dashboard.

Step 3: Request the Required Permissions

LinkedIn uses scopes to control API access.

Permission Description Approval required
r_liteprofile Basic profile: name and photo Auto-approved
r_emailaddress Email address Auto-approved
w_member_social Post on behalf of a user Partner verification
r_basicprofile Full profile Partner verification
r_organization_social Company-page access Partner verification
w_organization_social Post to a company page Partner verification
rw_ads Ads management Partner verification
r_ads_reporting Ads analytics Partner verification

Request only the scopes your application needs. This reduces approval friction and makes the consent screen clearer for users.

Step 4: Build the Authorization URL

Generate a random state value for every OAuth attempt. Store it in the user session, then verify it in the callback handler.

const crypto = require('crypto');

const getAuthUrl = (
  state,
  scopes = ['r_liteprofile', 'r_emailaddress']
) => {
  const params = new URLSearchParams({
    response_type: 'code',
    client_id: LINKEDIN_CLIENT_ID,
    redirect_uri: LINKEDIN_REDIRECT_URI,
    scope: scopes.join(' '),
    state
  });

  return `https://www.linkedin.com/oauth/v2/authorization?${params.toString()}`;
};

// Generate and persist this value in the user's session before redirecting.
const state = crypto.randomBytes(16).toString('hex');

const authUrl = getAuthUrl(state, [
  'r_liteprofile',
  'r_emailaddress',
  'w_member_social'
]);

console.log(`Redirect user to: ${authUrl}`);
Enter fullscreen mode Exit fullscreen mode

Step 5: Exchange the Authorization Code for an Access Token

After the user approves access, LinkedIn redirects to your callback URL with a code and state.

const exchangeCodeForToken = async (code) => {
  const response = await fetch(
    'https://www.linkedin.com/oauth/v2/accessToken',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        client_id: LINKEDIN_CLIENT_ID,
        client_secret: LINKEDIN_CLIENT_SECRET,
        redirect_uri: LINKEDIN_REDIRECT_URI
      })
    }
  );

  const data = await response.json();

  if (!response.ok) {
    throw new Error(data.error_description || 'Token exchange failed');
  }

  return {
    accessToken: data.access_token,
    expiresIn: data.expires_in // 60 days
  };
};
Enter fullscreen mode Exit fullscreen mode

Validate the returned state before exchanging the authorization code.

app.get('/oauth/callback', async (req, res) => {
  const { code, state } = req.query;

  if (!code || state !== req.session.linkedinOAuthState) {
    return res.status(400).send('Invalid OAuth callback');
  }

  try {
    const tokens = await exchangeCodeForToken(code);

    // Store tokens securely. Do not expose access tokens to the browser.
    await db.users.update(req.session.userId, {
      linkedin_access_token: tokens.accessToken,
      linkedin_token_expires: Date.now() + tokens.expiresIn * 1000
    });

    res.redirect('/success');
  } catch (error) {
    console.error('OAuth error:', error);
    res.status(500).send('Authentication failed');
  }
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Handle Token Expiry

LinkedIn access tokens expire after 60 days. LinkedIn does not provide refresh tokens, so users must authenticate again.

Check expiration before making API calls and notify users before their token expires.

const refreshAccessToken = async (refreshToken) => {
  // LinkedIn does not provide refresh tokens.
  // Users must re-authenticate after 60 days.
};
Enter fullscreen mode Exit fullscreen mode
const ensureValidToken = async (userId) => {
  const user = await db.users.findById(userId);

  // Notify the user when fewer than 24 hours remain.
  if (user.linkedin_token_expires < Date.now() + 86_400_000) {
    await notifyUserToReauth(user.id);
    throw new Error('Token expired or near expiry; re-authentication required');
  }

  return user.linkedin_access_token;
};
Enter fullscreen mode Exit fullscreen mode

Step 7: Create a Reusable API Client

Wrap LinkedIn requests in one helper so authorization, headers, and error handling stay consistent.

const LINKEDIN_BASE_URL = 'https://api.linkedin.com/v2';

const linkedinRequest = async (endpoint, options = {}) => {
  const { userId, headers = {}, ...fetchOptions } = options;
  const accessToken = await ensureValidToken(userId);

  const response = await fetch(`${LINKEDIN_BASE_URL}${endpoint}`, {
    ...fetchOptions,
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
      'X-Restli-Protocol-Version': '2.0.0',
      ...headers
    }
  });

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(`LinkedIn API error: ${error.message || response.status}`);
  }

  // Some successful write operations may not return JSON.
  if (response.status === 204) {
    return null;
  }

  return response.json();
};
Enter fullscreen mode Exit fullscreen mode

Example:

const profile = await linkedinRequest('/me', { userId: currentUser.id });

console.log(
  `Hello, ${profile.localizedFirstName} ${profile.localizedLastName}`
);
Enter fullscreen mode Exit fullscreen mode

Profile Access

Get the Authenticated User Profile

Use the /me endpoint to retrieve basic profile data for the authenticated user.

const getUserProfile = async (userId) => {
  return linkedinRequest(
    '/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))',
    { userId }
  );
};

const profile = await getUserProfile(currentUser.id);

console.log(`Name: ${profile.localizedFirstName} ${profile.localizedLastName}`);
console.log(`ID: ${profile.id}`);

const photoUrl =
  profile.profilePicture?.['displayImage~']?.elements?.[0]?.identifiers?.[0]
    ?.identifier;

console.log(`Photo: ${photoUrl}`);
Enter fullscreen mode Exit fullscreen mode

Get the User Email Address

Request the r_emailaddress scope, then call the email endpoint.

const getUserEmail = async (userId) => {
  return linkedinRequest(
    '/emailAddress?q=members&projection=(emailAddress*)',
    { userId }
  );
};

const email = await getUserEmail(currentUser.id);

console.log(`Email: ${email.elements?.[0]?.emailAddress}`);
Enter fullscreen mode Exit fullscreen mode

Profile Fields Available

Field Permission Description
id r_liteprofile LinkedIn member ID
firstName r_liteprofile First name
lastName r_liteprofile Last name
profilePicture r_liteprofile Profile photo URL
headline r_basicprofile Professional headline
summary r_basicprofile About section
positions r_basicprofile Work history
educations r_basicprofile Education history
emailAddress r_emailaddress Primary email

Content Posting

Create a Text Post

To publish a post, use the authenticated member URN as the author and request the w_member_social scope.

const createPost = async (userId, authorUrn, postContent) => {
  return linkedinRequest('/ugcPosts', {
    userId,
    method: 'POST',
    body: JSON.stringify({
      author: authorUrn,
      lifecycleState: 'PUBLISHED',
      specificContent: {
        'com.linkedin.ugc.ShareContent': {
          shareCommentary: {
            text: postContent.text
          },
          shareMediaCategory: 'NONE'
        }
      },
      visibility: {
        'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
      }
    })
  });
};

const post = await createPost(
  currentUser.id,
  'urn:li:person:ABC123',
  {
    text: 'Excited to announce our new product launch! 🚀 #innovation #startup'
  }
);

console.log(`Post created: ${post.id}`);
Enter fullscreen mode Exit fullscreen mode

Create a Post with an Image

Publishing image content is a three-step process:

  1. Register the media upload.
  2. Upload the image binary to the URL returned by LinkedIn.
  3. Create a UGC post that references the returned asset URN.
const createPostWithImage = async (userId, authorUrn, postData) => {
  // Step 1: Register the upload.
  const uploadRegistration = await linkedinRequest(
    '/assets?action=registerUpload',
    {
      userId,
      method: 'POST',
      body: JSON.stringify({
        registerUploadRequest: {
          recipes: ['urn:li:digitalmediaRecipe:feedshare-image'],
          owner: authorUrn,
          serviceRelationships: [
            {
              relationshipType: 'OWNER',
              identifier: 'urn:li:userGeneratedContent'
            }
          ]
        }
      })
    }
  );

  const uploadUrl =
    uploadRegistration.value.uploadMechanism[
      'com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest'
    ].uploadUrl;

  const assetUrn = uploadRegistration.value.asset;

  // Step 2: Upload the image bytes.
  const accessToken = await ensureValidToken(userId);

  await fetch(uploadUrl, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/octet-stream'
    },
    body: postData.imageBuffer
  });

  // Step 3: Publish the post.
  return linkedinRequest('/ugcPosts', {
    userId,
    method: 'POST',
    body: JSON.stringify({
      author: authorUrn,
      lifecycleState: 'PUBLISHED',
      specificContent: {
        'com.linkedin.ugc.ShareContent': {
          shareCommentary: {
            text: postData.text
          },
          shareMediaCategory: 'IMAGE',
          media: [
            {
              status: 'READY',
              description: {
                text: postData.imageDescription || ''
              },
              media: assetUrn,
              title: {
                text: postData.title || ''
              }
            }
          ]
        }
      },
      visibility: {
        'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
      }
    })
  });
};
Enter fullscreen mode Exit fullscreen mode

Create a Post with a Video

Video posting follows the same overall pattern: register the upload, upload the video through the returned URL, then publish a post that references the asset URN.

const createPostWithVideo = async (userId, authorUrn, postData) => {
  const uploadRegistration = await linkedinRequest(
    '/assets?action=registerUpload',
    {
      userId,
      method: 'POST',
      body: JSON.stringify({
        registerUploadRequest: {
          recipes: ['urn:li:digitalmediaRecipe:feedshare-video'],
          owner: authorUrn,
          serviceRelationships: [
            {
              relationshipType: 'OWNER',
              identifier: 'urn:li:userGeneratedContent'
            }
          ]
        }
      })
    }
  );

  const assetUrn = uploadRegistration.value.asset;

  // Upload the video using the presigned upload URLs in the response.
  // ... upload logic ...

  return linkedinRequest('/ugcPosts', {
    userId,
    method: 'POST',
    body: JSON.stringify({
      author: authorUrn,
      lifecycleState: 'PUBLISHED',
      specificContent: {
        'com.linkedin.ugc.ShareContent': {
          shareCommentary: { text: postData.text },
          shareMediaCategory: 'VIDEO',
          media: [{ status: 'READY', media: assetUrn }]
        }
      },
      visibility: {
        'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
      }
    })
  });
};
Enter fullscreen mode Exit fullscreen mode

Media Specifications

Media type Format Maximum size Duration
Image JPG, PNG, GIF 8 MB N/A
Video MP4, MOV 5 GB 15 minutes maximum
Document PDF, PPT, DOC 100 MB N/A

Company Page Management

Get Company Information

Use the organization ID to query company-page details.

const getCompanyInfo = async (userId, companyId) => {
  return linkedinRequest(
    `/organizations/${companyId}?projection=(id,localizedName,vanityName,tagline,description,universalName,logoV2(original~:playableStreams),companyType,companyPageUrl,confirmedLocations,industries,followerCount,staffCountRange,website,specialties)`,
    { userId }
  );
};

const company = await getCompanyInfo(currentUser.id, '1234567');

console.log(`Company: ${company.localizedName}`);
console.log(`Followers: ${company.followerCount}`);
console.log(`Website: ${company.website}`);
Enter fullscreen mode Exit fullscreen mode

Post to a Company Page

To post as an organization, use an organization URN and obtain the required organization permissions.

const createCompanyPost = async (userId, organizationUrn, postContent) => {
  return linkedinRequest('/ugcPosts', {
    userId,
    method: 'POST',
    body: JSON.stringify({
      author: organizationUrn,
      lifecycleState: 'PUBLISHED',
      specificContent: {
        'com.linkedin.ugc.ShareContent': {
          shareCommentary: {
            text: postContent.text
          },
          shareMediaCategory: postContent.media ? 'IMAGE' : 'NONE',
          media: postContent.media
            ? [
                {
                  status: 'READY',
                  media: postContent.media.assetUrn
                }
              ]
            : []
        }
      },
      visibility: {
        'com.linkedin.ugc.MemberNetworkVisibility': 'PUBLIC'
      }
    })
  });
};

const post = await createCompanyPost(
  currentUser.id,
  'urn:li:organization:1234567',
  {
    text: "We're hiring! Join our growing team. #careers #hiring"
  }
);
Enter fullscreen mode Exit fullscreen mode

Get Company Followers

const getFollowerCount = async (userId, organizationId) => {
  return linkedinRequest(
    `/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:${organizationId}`,
    { userId }
  );
};
Enter fullscreen mode Exit fullscreen mode

Rate Limiting

Understand Rate Limits

LinkedIn applies rate limits per app. Limits vary by API product.

API Limit Window
Profile API 100 requests Per day
UGC Posts 50 posts Per day
Company Admin 500 requests Per day
Ads API 100 requests Per minute

Inspect Rate-Limit Headers

Read quota headers from API responses and use them to control retries and queue work.

Header Description
X-Restli-Quota-Remaining Remaining requests
X-Restli-Quota-Reset Seconds until reset

For production workloads:

  • Queue writes such as post creation.
  • Cache profile and organization data when appropriate.
  • Avoid polling when webhooks can provide updates.
  • Log 429 responses and back off before retrying.

Troubleshooting Common Issues

401 Unauthorized

Check the following:

  • The access token has not expired.
  • The user has re-authenticated if the 60-day token lifetime has ended.
  • The token includes the scope required by the endpoint.
  • The request includes Authorization: Bearer <token>.

403 Forbidden

Check the following:

  • Your app has the required permissions.
  • The user approved the requested scopes.
  • The API product requires Partner verification.

429 Rate Limited

Reduce request volume:

  • Queue API requests.
  • Cache repeated reads.
  • Use webhooks instead of polling where possible.
  • Read X-Restli-Quota-Remaining and X-Restli-Quota-Reset before scheduling retries.

Production Deployment Checklist

Before going live:

  • [ ] Complete LinkedIn Partner verification where required.
  • [ ] Implement OAuth 2.0 with secure token storage.
  • [ ] Validate the OAuth state parameter.
  • [ ] Add token-expiry notifications for 60-day tokens.
  • [ ] Add request queuing and rate-limit handling.
  • [ ] Configure webhook endpoints.
  • [ ] Implement structured error handling.
  • [ ] Log API requests and failures without logging access tokens.
  • [ ] Review content against LinkedIn brand guidelines.

Real-World Use Cases

Recruitment Platform

A recruiting tool can automate job-posting workflows.

  • Challenge: Manual posting across multiple channels.
  • Solution: LinkedIn Jobs API integration.
  • Result: 80% time savings and 3x applications.

B2B Marketing Automation

A marketing platform can schedule LinkedIn content.

  • Challenge: Inconsistent posting schedules.
  • Solution: Automated posting through the UGC API.
  • Result: 5x engagement and more consistent brand presence.

Conclusion

A LinkedIn API integration can automate profile access, content publishing, and company-page workflows, but implementation depends on OAuth scopes, Partner access, and rate limits.

Key takeaways:

  • Use OAuth 2.0 and securely store 60-day access tokens.
  • Build a reusable API client for consistent headers and error handling.
  • Request only the permissions your app needs.
  • Use profile, posting, and company-page APIs based on your approved access.
  • Plan around rate limits of approximately 100–500 requests per day.
  • Use API testing tools such as Apidog to validate OAuth flows, endpoints, and team test scenarios.

FAQ Section

How do I get access to the LinkedIn API?

Create a LinkedIn Developer account, create an app, and complete Partner verification when you need advanced API access.

Can I post to LinkedIn automatically?

Yes. Use the UGC API with w_member_social for personal posts or w_organization_social for company posts.

What are LinkedIn rate limits?

Rate limits range from approximately 100–500 requests per day depending on the API. The Ads API allows 100 requests per minute.

How long do LinkedIn tokens last?

Access tokens expire after 60 days. Users must re-authenticate to continue API access.

Can I access user connections?

No. LinkedIn removed Connections API access for most apps because of privacy changes.

Top comments (1)

Collapse
 
topstar_ai profile image
Luis Cruz

I found the section on API rate limits particularly interesting, as it highlights the importance of handling these limits to avoid having your app throttled. The article mentions that the LinkedIn API has a rate limit of approximately 100-500 requests per day per app, which can be a challenge when dealing with large-scale integrations. In my experience, implementing a queue-based system to manage API requests and handle retries can help mitigate this issue. Have you explored any strategies for optimizing API request throughput while staying within these limits?