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.
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
- Go to the LinkedIn Developer Portal
- Sign in with your LinkedIn account.
- Click Create App in My Apps dashboard.
- 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
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}`);
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');
}
});
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;
};
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}`);
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}`);
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 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}`);
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;
};
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 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"
});
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
| 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:
- Access token has not expired (max 60 days).
- Token scope includes requested resource.
-
Authorization: Bearer {token}header is present.
Issue: 403 Forbidden
Check:
- App has required permissions.
- User has approved requested scopes.
- Partner verification completed.
Issue: 429 Rate Limited
Mitigate by:
- Implementing request queuing.
- Caching responses.
- 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)