LinkedIn API integration with managed OAuth. Share posts, manage profile, run ads, and access LinkedIn features. Use this skill when users want to share cont...
LinkedIn API integration with managed OAuth. Share posts, manage profile, run ads, and access LinkedIn features. Use this skill when users want to share cont...
Real data. Real impact.
Emerging
Developers
Per week
Open source
Skills give you superpowers. Install in 30 seconds.
Access the LinkedIn API with managed OAuth authentication. Share posts, manage advertising campaigns, retrieve profile and organization information, upload media, and access the Ad Library.
# Get current user profile python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://gateway.maton.ai/linkedin/rest/me') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') req.add_header('LinkedIn-Version', '202506') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
https://gateway.maton.ai/linkedin/rest/{resource}
The gateway proxies requests to api.linkedin.com and automatically injects your OAuth token.
All requests require the Maton API key in the Authorization header:
Authorization: Bearer $MATON_API_KEY
Environment Variable: Set your API key as MATON_API_KEY:
export MATON_API_KEY="YOUR_API_KEY"
LinkedIn REST API requires the version header:
LinkedIn-Version: 202506
Manage your LinkedIn OAuth connections at https://ctrl.maton.ai.
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://ctrl.maton.ai/connections?app=linkedin&status=ACTIVE') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
python <<'EOF' import urllib.request, os, json data = json.dumps({'app': 'linkedin'}).encode() req = urllib.request.Request('https://ctrl.maton.ai/connections', data=data, method='POST') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') req.add_header('Content-Type', 'application/json') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
Response:
{ "connection": { "connection_id": "ba10eb9e-b590-4e95-8c2e-3901ff94642a", "status": "ACTIVE", "creation_time": "2026-02-07T08:00:24.372659Z", "last_updated_time": "2026-02-07T08:05:16.609085Z", "url": "https://connect.maton.ai/?session_token=...", "app": "linkedin", "metadata": {} } }
Open the returned url in a browser to complete OAuth authorization.
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://ctrl.maton.ai/connections/{connection_id}', method='DELETE') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
If you have multiple LinkedIn connections, specify which one to use with the Maton-Connection header:
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://gateway.maton.ai/linkedin/rest/me') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') req.add_header('LinkedIn-Version', '202506') req.add_header('Maton-Connection', 'ba10eb9e-b590-4e95-8c2e-3901ff94642a') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
If omitted, the gateway uses the default (oldest) active connection.
GET /linkedin/rest/me LinkedIn-Version: 202506
Example:
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://gateway.maton.ai/linkedin/rest/me') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') req.add_header('LinkedIn-Version', '202506') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
Response:
{ "firstName": { "localized": {"en_US": "John"}, "preferredLocale": {"country": "US", "language": "en"} }, "localizedFirstName": "John", "lastName": { "localized": {"en_US": "Doe"}, "preferredLocale": {"country": "US", "language": "en"} }, "localizedLastName": "Doe", "id": "yrZCpj2Z12", "vanityName": "johndoe", "localizedHeadline": "Software Engineer at Example Corp", "profilePicture": { "displayImage": "urn:li:digitalmediaAsset:C4D00AAAAbBCDEFGhiJ" } }
POST /linkedin/rest/posts Content-Type: application/json LinkedIn-Version: 202506 { "author": "urn:li:person:{personId}", "lifecycleState": "PUBLISHED", "visibility": "PUBLIC", "commentary": "Hello LinkedIn! This is my first API post.", "distribution": { "feedDistribution": "MAIN_FEED" } }
Response: 201 Created with x-restli-id header containing the post URN.
POST /linkedin/rest/posts Content-Type: application/json LinkedIn-Version: 202506 { "author": "urn:li:person:{personId}", "lifecycleState": "PUBLISHED", "visibility": "PUBLIC", "commentary": "Check out this great article!", "distribution": { "feedDistribution": "MAIN_FEED" }, "content": { "article": { "source": "https://example.com/article", "title": "Article Title", "description": "Article description here" } } }
First, initialize the image upload, then upload the image, then create the post.
Step 1: Initialize Image Upload
POST /linkedin/rest/images?action=initializeUpload Content-Type: application/json LinkedIn-Version: 202506 { "initializeUploadRequest": { "owner": "urn:li:person:{personId}" } }
Response:
{ "value": { "uploadUrlExpiresAt": 1770541529250, "uploadUrl": "https://www.linkedin.com/dms-uploads/...", "image": "urn:li:image:D4D10AQH4GJAjaFCkHQ" } }
Step 2: Upload Image Binary
PUT {uploadUrl from step 1} Content-Type: image/png {binary image data}
Step 3: Create Image Post
POST /linkedin/rest/posts Content-Type: application/json LinkedIn-Version: 202506 { "author": "urn:li:person:{personId}", "lifecycleState": "PUBLISHED", "visibility": "PUBLIC", "commentary": "Check out this image!", "distribution": { "feedDistribution": "MAIN_FEED" }, "content": { "media": { "id": "urn:li:image:D4D10AQH4GJAjaFCkHQ", "title": "Image Title" } } }
| Value | Description |
|---|---|
| PUBLIC | Viewable by anyone on LinkedIn |
| CONNECTIONS | Viewable by 1st-degree connections only |
| Value | Description |
|---|---|
| NONE | Text-only post |
| ARTICLE | URL/article share |
| IMAGE | Image post |
| VIDEO | Video post |
The Ad Library API provides access to public advertising data on LinkedIn. These endpoints use the REST API with version headers.
LinkedIn-Version: 202506
GET /linkedin/rest/adLibrary?q=criteria&keyword={keyword}
Query parameters:
Example - Search ads by keyword:
GET /linkedin/rest/adLibrary?q=criteria&keyword=linkedin
Example - Search ads by advertiser:
GET /linkedin/rest/adLibrary?q=criteria&advertiser=microsoft
Response:
{ "paging": { "start": 0, "count": 10, "total": 11619543, "links": [...] }, "elements": [ { "adUrl": "https://www.linkedin.com/ad-library/detail/...", "details": { "advertiser": {...}, "adType": "TEXT_AD", "targeting": {...}, "statistics": { "firstImpressionDate": 1704067200000, "latestImpressionDate": 1706745600000, "impressionsFrom": 1000, "impressionsTo": 5000 } }, "isRestricted": false } ] }
GET /linkedin/rest/jobLibrary?q=criteria&keyword={keyword}
Note: Job Library requires version 202506.
Query parameters:
Example:
GET /linkedin/rest/jobLibrary?q=criteria&keyword=software&organization=google
Response includes:
The Marketing API provides access to LinkedIn's advertising platform. These endpoints use the versioned REST API.
LinkedIn-Version: 202506
GET /linkedin/rest/adAccounts?q=search
Returns all ad accounts accessible by the authenticated user.
Response:
{ "paging": { "start": 0, "count": 10, "links": [] }, "elements": [ { "id": 123456789, "name": "My Ad Account", "status": "ACTIVE", "type": "BUSINESS", "currency": "USD", "reference": "urn:li:organization:12345" } ] }
GET /linkedin/rest/adAccounts/{adAccountId}
POST /linkedin/rest/adAccounts Content-Type: application/json { "name": "New Ad Account", "currency": "USD", "reference": "urn:li:organization:{orgId}", "type": "BUSINESS" }
POST /linkedin/rest/adAccounts/{adAccountId} Content-Type: application/json X-RestLi-Method: PARTIAL_UPDATE { "patch": { "$set": { "name": "Updated Account Name" } } }
Campaign groups are nested under ad accounts:
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups Content-Type: application/json { "name": "Q1 2026 Campaigns", "status": "DRAFT", "runSchedule": { "start": 1704067200000, "end": 1711929600000 }, "totalBudget": { "amount": "10000", "currencyCode": "USD" } }
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId} Content-Type: application/json X-RestLi-Method: PARTIAL_UPDATE { "patch": { "$set": { "status": "ACTIVE" } } }
DELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}
Campaigns are also nested under ad accounts:
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns Content-Type: application/json { "campaignGroup": "urn:li:sponsoredCampaignGroup:123456", "name": "Brand Awareness Campaign", "status": "DRAFT", "type": "SPONSORED_UPDATES", "objectiveType": "BRAND_AWARENESS", "dailyBudget": { "amount": "100", "currencyCode": "USD" }, "costType": "CPM", "unitCost": { "amount": "5", "currencyCode": "USD" }, "locale": { "country": "US", "language": "en" } }
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId} Content-Type: application/json X-RestLi-Method: PARTIAL_UPDATE { "patch": { "$set": { "status": "ACTIVE" } } }
DELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}
| Status | Description |
|---|---|
| DRAFT | Campaign is in draft mode |
| ACTIVE | Campaign is running |
| PAUSED | Campaign is paused |
| ARCHIVED | Campaign is archived |
| COMPLETED | Campaign has ended |
| CANCELED | Campaign was canceled |
| Objective | Description |
|---|---|
| BRAND_AWARENESS | Increase brand visibility |
| WEBSITE_VISITS | Drive traffic to website |
| ENGAGEMENT | Increase post engagement |
| VIDEO_VIEWS | Maximize video views |
| LEAD_GENERATION | Collect leads via Lead Gen Forms |
| WEBSITE_CONVERSIONS | Drive website conversions |
| JOB_APPLICANTS | Attract job applications |
Get organizations the authenticated user has access to:
GET /linkedin/rest/organizationAcls?q=roleAssignee LinkedIn-Version: 202506
Response:
{ "paging": { "start": 0, "count": 10, "total": 2 }, "elements": [ { "role": "ADMINISTRATOR", "organization": "urn:li:organization:12345", "state": "APPROVED" } ] }
GET /linkedin/rest/organizations/{organizationId} LinkedIn-Version: 202506
GET /linkedin/rest/organizations?q=vanityName&vanityName={vanityName}
Example:
GET /linkedin/rest/organizations?q=vanityName&vanityName=microsoft
Response:
{ "elements": [ { "vanityName": "microsoft", "localizedName": "Microsoft", "website": { "localized": {"en_US": "https://news.microsoft.com/"} } } ] }
GET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity={orgUrn}
Example:
GET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:12345
GET /linkedin/rest/posts?q=author&author={orgUrn}
Example:
GET /linkedin/rest/posts?q=author&author=urn:li:organization:12345
The REST API provides modern media upload endpoints. All require version header LinkedIn-Version: 202506.
POST /linkedin/rest/images?action=initializeUpload Content-Type: application/json LinkedIn-Version: 202506 { "initializeUploadRequest": { "owner": "urn:li:person:{personId}" } }
Response:
{ "value": { "uploadUrlExpiresAt": 1770541529250, "uploadUrl": "https://www.linkedin.com/dms-uploads/...", "image": "urn:li:image:D4D10AQH4GJAjaFCkHQ" } }
Use the uploadUrl to PUT your image binary, then use the image URN in your post.
Video uploads are a 4-step process: initialize, upload binary, finalize, then create the post.
CRITICAL — URL Encoding: The upload URL returned by the initialize step contains URL-encoded characters (e.g., %253D) that get corrupted when passed through shell variables or curl. You MUST use Python urllib for the entire flow — parse the JSON response and use the URL directly in Python without passing it through the shell. This is the only reliable approach.
Complete working example:
python <<'EOF' import urllib.request, os, json GATEWAY = 'https://gateway.maton.ai' HEADERS = { 'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}', 'Content-Type': 'application/json', 'LinkedIn-Version': '202506', 'X-Restli-Protocol-Version': '2.0.0', } # Step 0: Get person ID req = urllib.request.Request(f'{GATEWAY}/linkedin/rest/me') for k, v in HEADERS.items(): req.add_header(k, v) person_id = json.load(urllib.request.urlopen(req))['id'] owner = f'urn:li:person:{person_id}' # Step 1: Initialize upload (via gateway) file_path = '/path/to/video.mp4' file_size = os.path.getsize(file_path) init_data = json.dumps({ 'initializeUploadRequest': { 'owner': owner, 'fileSizeBytes': file_size, 'uploadCaptions': False, 'uploadThumbnail': False, } }).encode() req = urllib.request.Request(f'{GATEWAY}/linkedin/rest/videos?action=initializeUpload', data=init_data, method='POST') for k, v in HEADERS.items(): req.add_header(k, v) init_resp = json.load(urllib.request.urlopen(req)) upload_url = init_resp['value']['uploadInstructions'][0]['uploadUrl'] video_urn = init_resp['value']['video'] # Step 2: Upload binary DIRECTLY to LinkedIn's pre-signed URL (NOT through the gateway) # The upload URL points to www.linkedin.com — it is pre-signed and needs NO Authorization header. # IMPORTANT: Use the URL exactly as returned by json.load() — do NOT pass it through shell variables. with open(file_path, 'rb') as f: video_data = f.read() upload_req = urllib.request.Request(upload_url, data=video_data, method='PUT') upload_req.add_header('Content-Type', 'application/octet-stream') upload_resp = urllib.request.urlopen(upload_req) etag = upload_resp.headers['etag'] # Step 3: Finalize upload (via gateway) finalize_data = json.dumps({ 'finalizeUploadRequest': { 'video': video_urn, 'uploadToken': '', 'uploadedPartIds': [etag], } }).encode() req = urllib.request.Request(f'{GATEWAY}/linkedin/rest/videos?action=finalizeUpload', data=finalize_data, method='POST') for k, v in HEADERS.items(): req.add_header(k, v) urllib.request.urlopen(req) # Step 4: Create post with video (via gateway) post_data = json.dumps({ 'author': owner, 'lifecycleState': 'PUBLISHED', 'visibility': 'PUBLIC', 'commentary': 'Check out this video!', 'distribution': {'feedDistribution': 'MAIN_FEED'}, 'content': {'media': {'id': video_urn}}, }).encode() req = urllib.request.Request(f'{GATEWAY}/linkedin/rest/posts', data=post_data, method='POST') for k, v in HEADERS.items(): req.add_header(k, v) resp = urllib.request.urlopen(req) print(f'Video post created! {resp.headers.get("location")}') EOF
How it works:
Video specifications:
POST /linkedin/rest/documents?action=initializeUpload Content-Type: application/json LinkedIn-Version: 202506 { "initializeUploadRequest": { "owner": "urn:li:person:{personId}" } }
Response:
{ "value": { "uploadUrlExpiresAt": 1770541530896, "uploadUrl": "https://www.linkedin.com/dms-uploads/...", "document": "urn:li:document:D4D10AQHr-e30QZCAjQ" } }
GET /linkedin/rest/adTargetingFacets
Returns all available targeting facets for ad campaigns (31 facets including employers, degrees, skills, locations, industries, etc.).
Response:
{ "elements": [ { "facetName": "skills", "adTargetingFacetUrn": "urn:li:adTargetingFacet:skills", "entityTypes": ["SKILL"], "availableEntityFinders": ["AD_TARGETING_FACET", "TYPEAHEAD"] }, { "facetName": "industries", "adTargetingFacetUrn": "urn:li:adTargetingFacet:industries" } ] }
Available targeting facets include:
To create posts, you need your LinkedIn person ID. Get it from the /rest/me endpoint:
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://gateway.maton.ai/linkedin/rest/me') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') req.add_header('LinkedIn-Version', '202506') result = json.load(urllib.request.urlopen(req)) print(f"Your person URN: urn:li:person:{result['id']}") EOF
const personId = 'YOUR_PERSON_ID'; const response = await fetch( 'https://gateway.maton.ai/linkedin/rest/posts', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.MATON_API_KEY}`, 'Content-Type': 'application/json', 'LinkedIn-Version': '202506' }, body: JSON.stringify({ author: `urn:li:person:${personId}`, lifecycleState: 'PUBLISHED', visibility: 'PUBLIC', commentary: 'Hello from the API!', distribution: { feedDistribution: 'MAIN_FEED' } }) } );
import os import requests person_id = 'YOUR_PERSON_ID' response = requests.post( 'https://gateway.maton.ai/linkedin/rest/posts', headers={ 'Authorization': f'Bearer {os.environ["MATON_API_KEY"]}', 'Content-Type': 'application/json', 'LinkedIn-Version': '202506' }, json={ 'author': f'urn:li:person:{person_id}', 'lifecycleState': 'PUBLISHED', 'visibility': 'PUBLIC', 'commentary': 'Hello from the API!', 'distribution': { 'feedDistribution': 'MAIN_FEED' } } )
| Throttle Type | Daily Limit (UTC) |
|---|---|
| Member | 150 requests/day |
| Application | 100,000 requests/day |
The commentary field in posts uses LinkedIn's "Little Text Format". Reserved characters must be escaped with a backslash or the post content will be truncated.
| Character | Escape As |
|---|---|
| \ | \ |
| { | { |
| } | } |
| @ | @ |
| [ | [ |
| ] | ] |
| ( | ( |
| ) | ) |
| < | < |
| > | > |
| # | # |
| * | * |
| _ | _ |
| ~ | ~ |
{ "commentary": "Hello\\! Check out these bullet points:\\n\\n\\* Point 1\\n\\* Point 2\\n\\* More info \\(details inside\\)" }
Use Little Text Format syntax for mentions and hashtags:
def escape_linkedin_commentary(text): """Escape reserved characters for LinkedIn Little Text Format.""" reserved = ['\\', '|', '{', '}', '@', '[', ']', '(', ')', '<', '>', '#', '*', '_', '~'] for char in reserved: text = text.replace(char, '\\' + char) return text # Usage commentary = escape_linkedin_commentary("Check this out! Details (inside) #tech") # Result: "Check this out\\! Details \\(inside\\) \\#tech"
| Status | Meaning |
|---|---|
| 400 | Missing LinkedIn connection or invalid request |
| 401 | Invalid or missing Maton API key |
| 403 | Insufficient permissions (check OAuth scopes) |
| 404 | Resource not found |
| 422 | Invalid request body or URN format |
| 429 | Rate limited |
| 4xx/5xx | Passthrough error from LinkedIn API |
{ "status": 403, "serviceErrorCode": 100, "code": "ACCESS_DENIED", "message": "Not enough permissions to access resource" }
echo $MATON_API_KEY
python <<'EOF' import urllib.request, os, json req = urllib.request.Request('https://ctrl.maton.ai/connections') req.add_header('Authorization', f'Bearer {os.environ["MATON_API_KEY"]}') print(json.dumps(json.load(urllib.request.urlopen(req)), indent=2)) EOF
Ensure your URL path starts with linkedin. For example:
Incorrect: https://gateway.maton.ai/rest/me
| Scope | Description |
|---|---|
| openid | OpenID Connect authentication |
| profile | Read basic profile |
| Read email address | |
| w_member_social | Create, modify, and delete posts |
No automatic installation available. Please visit the source repository for installation instructions.
View Installation Instructions1,500+ AI skills, agents & workflows. Install in 30 seconds. Part of the Torly.ai family.
© 2026 Torly.ai. All rights reserved.