DEV Community

Cover image for How to Use LinkedIn API: Complete Professional Network Integration Guide (2026)
Wanda
Wanda

Posted on • Originally published at apidog.com

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

TL;DR

The LinkedIn API lets you programmatically access LinkedIn’s professional network. It uses OAuth 2.0 authentication, supports both RESTful and GraphQL endpoints for profiles, posts, comments, company pages, and ads, and enforces rate limits (100-500 requests per day per app). This guide covers step-by-step: authentication, profile retrieval, content posting, company page management, ads API integration, and production deployment.

Try Apidog today


Introduction

LinkedIn reaches over 900 million professional users worldwide. If you’re building recruiting tools, marketing platforms, or B2B apps, integrating with the LinkedIn API is essential for automation and scale.

Manual management of LinkedIn presence wastes 15-20 hours per week for B2B marketers. With the LinkedIn API, you can automate content publishing, lead capture, analytics, and recruiting workflows.

This guide provides actionable steps for LinkedIn API integration: OAuth 2.0 setup, profile access, posting, company management, ads, webhooks, and production-readiness.

💡 Apidog simplifies API integration testing. Test LinkedIn endpoints, validate OAuth, inspect API responses, and debug permissions in a single workspace. Import specs, mock responses, and share tests with your team.

What Is the LinkedIn API?

LinkedIn offers RESTful and GraphQL APIs for professional data. Core use cases:

  • User profile access (with consent)
  • Company pages and updates (CRUD)
  • Posts, comments, reactions
  • Limited connections access
  • Job postings & applications
  • LinkedIn Ads management
  • Lead gen forms
  • Messaging (partner-only)

Key Features

Feature Description
RESTful + GraphQL Multiple API styles
OAuth 2.0 User authorization required
Rate Limiting 100-500 requests/day
Company Pages Full 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 profile
Company Admin API Partner Manage company pages
Ads API Partner Ad campaign management
Job Posting API Partner Post/manage jobs
Marketing Developer Platform Partner Full API access

API Versions

Version Status End Date
v2 Current Active
v1 Retired Dec 2023

Getting Started: Authentication Setup

Step 1: Create LinkedIn Developer Account

  1. Go to the LinkedIn Developer Portal
  2. Sign in with your LinkedIn account.
  3. Click Create App in My Apps dashboard.
  4. Fill in app details (name, logo, description).

Step 2: Configure App Settings

Set authentication variables in your environment:

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

Step 3: Request Required Permissions

LinkedIn permissions (scopes):

Permission Description Approval Required
r_liteprofile Basic profile Auto-approved
r_emailaddress Email address Auto-approved
w_member_social Post as user Partner verification
r_basicprofile Full profile Partner verification
r_organization_social Company page access Partner verification
w_organization_social Post to company page Partner verification
rw_ads Ads management Partner verification
r_ads_reporting Ads analytics Partner verification

Step 4: Build Authorization URL

Implement OAuth 2.0 flow:

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: state
  });

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

// Usage
const state = require('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 Code for Access Token

Handle OAuth callback and get the access token:

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: code,
      client_id: LINKEDIN_CLIENT_ID,
      client_secret: LINKEDIN_CLIENT_SECRET,
      redirect_uri: LINKEDIN_REDIRECT_URI
    })
  });

  const data = await response.json();

  return {
    accessToken: data.access_token,
    expiresIn: data.expires_in // 60 days
  };
};

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

  try {
    const tokens = await exchangeCodeForToken(code);

    // Store tokens securely
    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: Refresh Access Token

LinkedIn access tokens expire after 60 days (no refresh tokens):

const refreshAccessToken = async (refreshToken) => {
  // LinkedIn does NOT offer refresh tokens.
  // Users must re-authenticate after 60 days.
  // Notify user before expiry.
};

const ensureValidToken = async (userId) => {
  const user = await db.users.findById(userId);

  if (user.linkedin_token_expires < Date.now() + 86400000) { // 24 hours
    await notifyUserToReauth(user.id);
    throw new Error('Token expired, please re-authenticate');
  }

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

Step 7: Make Authenticated API Calls

Reusable API client:

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

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

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

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

  return response.json();
};

// Usage
const profile = await linkedinRequest('/me');
console.log(`Hello, ${profile.localizedFirstName} ${profile.localizedLastName}`);
Enter fullscreen mode Exit fullscreen mode

Profile Access

Getting User Profile

Fetch the authenticated user's profile:

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

// Usage
const profile = await getUserProfile();
console.log(`Name: ${profile.localizedFirstName} ${profile.localizedLastName}`);
console.log(`ID: ${profile.id}`);
console.log(`Photo: ${profile.profilePicture?.['displayImage~']?.elements?.[0]?.identifiers?.[0]?.identifier}`);
Enter fullscreen mode Exit fullscreen mode

Getting Email Address

Fetch user's email:

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

// Usage
const email = await getUserEmail();
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

Creating a Post

Share a text post to the user’s feed:

const createPost = async (authorUrn, postContent) => {
  const response = await linkedinRequest('/ugcPosts', {
    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'
      }
    })
  });

  return response;
};

// Usage
const post = await createPost('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

Creating a Post with Image

Share a post with an uploaded image:

const createPostWithImage = async (authorUrn, postData) => {
  // Step 1: Register media upload
  const uploadRegistration = await linkedinRequest('/assets?action=registerUpload', {
    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 image
  await fetch(uploadUrl, {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + await getAccessToken(),
      'Content-Type': 'application/octet-stream'
    },
    body: postData.imageBuffer
  });

  // Step 3: Create post with image
  const post = await linkedinRequest('/ugcPosts', {
    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'
      }
    })
  });

  return post;
};
Enter fullscreen mode Exit fullscreen mode

Creating a Post with Video

Share video content:

const createPostWithVideo = async (authorUrn, postData) => {
  // Register video upload
  const uploadRegistration = await linkedinRequest('/assets?action=registerUpload', {
    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 video (use presigned upload URLs from response)
  // ... upload logic ...

  // Create post
  const post = await linkedinRequest('/ugcPosts', {
    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' }
    })
  });

  return post;
};
Enter fullscreen mode Exit fullscreen mode

Media Specifications

Media Type Format Max Size Duration
Image JPG, PNG, GIF 8MB N/A
Video MP4, MOV 5GB 15 min max
Document PDF, PPT, DOC 100MB N/A

Company Page Management

Getting Company Information

Fetch company page details:

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

// Usage
const company = await getCompanyInfo('1234567');
console.log(`Company: ${company.localizedName}`);
console.log(`Followers: ${company.followerCount}`);
console.log(`Website: ${company.website}`);
Enter fullscreen mode Exit fullscreen mode

Posting to Company Page

Share an update to a company page:

const createCompanyPost = async (organizationUrn, postContent) => {
  const response = await linkedinRequest('/ugcPosts', {
    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'
      }
    })
  });

  return response;
};

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

Getting Company Followers

Fetch follower count:

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

Rate Limiting

Understanding Rate Limits

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

Rate Limit Headers

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

Troubleshooting Common Issues

Issue: 401 Unauthorized

Check:

  1. Access token has not expired (max 60 days).
  2. Token scope includes requested resource.
  3. Authorization: Bearer {token} header is present.

Issue: 403 Forbidden

Check:

  1. App has required permissions.
  2. User has approved requested scopes.
  3. Partner verification completed.

Issue: 429 Rate Limited

Mitigate by:

  1. Implementing request queuing.
  2. Caching responses.
  3. Using webhooks instead of polling.

Production Deployment Checklist

Before launch, ensure:

  • [ ] Partner verification completed
  • [ ] OAuth 2.0 with secure token storage
  • [ ] Token expiry notifications (60 days)
  • [ ] Rate limiting and queueing
  • [ ] Webhook endpoints configured
  • [ ] Error handling implemented
  • [ ] API call logging added
  • [ ] Brand guidelines compliance reviewed

Real-World Use Cases

Recruitment Platform

  • Challenge: Manual job posting on multiple channels.
  • Solution: Automate with LinkedIn Jobs API.
  • Result: 80% time savings, 3x more applications.

B2B Marketing Automation

  • Challenge: Inconsistent posting schedule.
  • Solution: Schedule automated posting via UGC API.
  • Result: 5x engagement, consistent branding.

Conclusion

The LinkedIn API offers robust access to professional network features. Key points:

  • OAuth 2.0 authentication (60-day tokens)
  • Profile, posting, company page APIs
  • Strict rate limits (100-500/day)
  • Partner verification needed for most APIs
  • Tools like Apidog streamline API testing and team workflows.

FAQ Section

How do I get access to LinkedIn API?

Create a LinkedIn Developer account, register your app, and complete Partner verification for advanced access.

Can I post to LinkedIn automatically?

Yes. Use the UGC API with w_member_social (personal) or w_organization_social (company) permissions.

What are LinkedIn rate limits?

Varies by API: typically 100-500 requests per day. Ads API allows 100 requests per minute.

How long do LinkedIn tokens last?

60 days. Users must re-authenticate after expiry.

Can I access user connections?

No. Connections API access is restricted for most apps due to privacy changes.

Top comments (0)