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.
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:
- Visit the LinkedIn Developer Portal.
- Sign in with your LinkedIn account.
- Open My Apps.
- Select Create App.
- 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
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}`);
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
};
};
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');
}
});
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.
};
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;
};
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();
};
Example:
const profile = await linkedinRequest('/me', { userId: currentUser.id });
console.log(
`Hello, ${profile.localizedFirstName} ${profile.localizedLastName}`
);
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}`);
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}`);
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}`);
Create a Post with an Image
Publishing image content is a three-step process:
- Register the media upload.
- Upload the image binary to the URL returned by LinkedIn.
- 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'
}
})
});
};
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'
}
})
});
};
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}`);
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"
}
);
Get Company Followers
const getFollowerCount = async (userId, organizationId) => {
return linkedinRequest(
`/organizationalEntityFollowerStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:${organizationId}`,
{ userId }
);
};
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
429responses 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-RemainingandX-Restli-Quota-Resetbefore 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
stateparameter. - [ ] 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)
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?