TL;DR
The LinkedIn API enables developers to integrate with LinkedIn’s professional network programmatically. It uses OAuth 2.0 authentication, RESTful and GraphQL endpoints for profiles, posts, comments, company pages, and ads, with rate limits of 100-500 requests per day per app. This guide covers authentication setup, profile access, content posting, company page management, ads API, and production integration strategies.
Introduction
LinkedIn has over 900 million professional users across 200+ countries. For developers building recruiting tools, marketing platforms, or B2B applications, LinkedIn API integration is essential for reaching this professional audience.
Here’s the reality: B2B marketers managing LinkedIn presence manually lose 15-20 hours weekly on posting, engagement tracking, and lead generation. A solid LinkedIn API integration automates content distribution, lead capture, engagement analytics, and recruitment workflows.
This guide walks through the complete LinkedIn API integration process. You’ll learn OAuth 2.0 authentication, profile access, content posting, company page management, ads integration, webhooks, and production deployment strategies. By the end, you’ll have a production-ready LinkedIn integration.
What Is the LinkedIn API?
LinkedIn provides RESTful and GraphQL APIs for accessing professional network data. The API handles:
- User profile information (with consent)
- Company pages and updates
- Posts, comments, and reactions
- Connections (limited)
- Job postings and applications
- LinkedIn Ads management
- Lead generation forms
- Messaging (limited partners only)
Key Features
| Feature | Description |
|---|---|
| RESTful + GraphQL | Multiple API styles |
| OAuth 2.0 | User authorization required |
| Rate Limiting | 100-500 requests/day per app |
| 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 and manage jobs |
| Marketing Developer Platform | Partner | Full API access |
API Versions
| Version | Status | End Date |
|---|---|---|
| v2 | Current | Active |
| v1 | Retired | December 2023 |
Getting Started: Authentication Setup
Step 1: Create LinkedIn Developer Account
Before accessing the API:
- Visit the LinkedIn Developer Portal
- Sign in with LinkedIn account
- Click Create App in My Apps dashboard
- Fill in app details (name, logo, description)
Step 2: Configure App Settings
Set up app authentication:
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
Step 3: Request Required Permissions
LinkedIn requires permission approval:
| Permission | Description | Approval Required |
|---|---|---|
r_liteprofile |
Basic profile (name, photo) | Auto-approved |
r_emailaddress |
Email address | Auto-approved |
w_member_social |
Post on behalf of 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}`);
Step 5: Exchange Code for Access Token
Handle OAuth callback:
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
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');
}
});
Step 6: Refresh Access Token
Access tokens expire after 60 days:
const refreshAccessToken = async (refreshToken) => {
// Note: LinkedIn doesn't provide refresh tokens
// Users must re-authenticate after 60 days
// Implement expiry notification
};
// Check token expiry before API calls
const ensureValidToken = async (userId) => {
const user = await db.users.findById(userId);
if (user.linkedin_token_expires < Date.now() + 86400000) { // 24 hours
// Notify user to re-authenticate
await notifyUserToReauth(user.id);
throw new Error('Token expired, please re-authenticate');
}
return user.linkedin_access_token;
};
Step 7: Make Authenticated API Calls
Create 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}`);
Profile Access
Getting User Profile
Fetch 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}`);
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}`);
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 text post to 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}`);
Creating a Post with Image
Share post with media:
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 to provided URL
await fetch(uploadUrl, {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + await getAccessToken(),
'Content-Type': 'application/octet-stream'
},
body: postData.imageBuffer
});
// Step 3: Create post with uploaded 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;
};
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;
};
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}`);
Posting to Company Page
Share update to 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'
});
Getting Company Followers
Fetch follower count:
const getFollowerCount = async (organizationId) => {
const response = await linkedinRequest(
`/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:${organizationId}`
);
return response;
};
Rate Limiting
Understanding Rate Limits
LinkedIn enforces rate limits per app:
| 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
Solutions:
- Verify access token hasn’t expired (60 days)
- Check token scope includes requested resource
- Ensure
Authorization: Bearer {token}header present
Issue: 403 Forbidden
Solutions:
- Verify app has required permissions
- Check user approved requested scopes
- Partner verification may be required
Issue: 429 Rate Limited
Solutions:
- Implement request queuing
- Cache responses to reduce calls
- Use webhooks instead of polling
Production Deployment Checklist
Before going live:
- [ ] Complete LinkedIn Partner verification
- [ ] Implement OAuth 2.0 with secure token storage
- [ ] Add token expiry notifications (60 days)
- [ ] Set up rate limiting and queuing
- [ ] Configure webhook endpoints
- [ ] Implement comprehensive error handling
- [ ] Add logging for all API calls
- [ ] Create brand guidelines compliance review
Real-World Use Cases
Recruitment Platform
A recruiting tool automates job postings:
- Challenge: Manual posting across multiple channels
- Solution: LinkedIn Jobs API integration
- Result: 80% time savings, 3x applications
B2B Marketing Automation
A marketing platform schedules LinkedIn content:
- Challenge: Inconsistent posting schedule
- Solution: Automated posting via UGC API
- Result: 5x engagement, consistent brand presence
Conclusion
The LinkedIn API provides comprehensive access to professional network features. Key takeaways:
- OAuth 2.0 authentication with 60-day tokens
- Profile, posting, and company page APIs available
- Rate limits require careful management (100-500/day)
- Partner verification required for most APIs
- Apidog streamlines API testing and team collaboration
FAQ Section
How do I get access to LinkedIn API?
Create a LinkedIn Developer account, create an app, and complete Partner verification for advanced API access.
Can I post to LinkedIn automatically?
Yes, use the UGC (User Generated Content) API with w_member_social permission for personal posts or w_organization_social for company posts.
What are LinkedIn rate limits?
Rate limits range from 100-500 requests per day depending on the API. 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 due to privacy changes.



