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.
Reference
Get Current User Profile
GET /linkedin/rest/me
LinkedIn-Version: 202506Example:
python <<'EOF'
import urllib.request, os, json
req = urllib.request.Request('https://api.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))
EOFResponse:
{
"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"
}
}Create a Text Post
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.
Create an Article/URL Share
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"
}
}
}Create an Image Post
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"
}
}
}Visibility Options
| Value | Description |
|---|---|
PUBLIC | Viewable by anyone on LinkedIn |
CONNECTIONS | Viewable by 1st-degree connections only |
Share Media Categories
| Value | Description |
|---|---|
NONE | Text-only post |
ARTICLE | URL/article share |
IMAGE | Image post |
VIDEO | Video post |
Required Headers for Ad Library
LinkedIn-Version: 202506Search Ads
GET /linkedin/rest/adLibrary?q=criteria&keyword={keyword}Query parameters:
keyword(string): Search ad content (multiple keywords use AND logic)advertiser(string): Search by advertiser namecountries(array): Filter by ISO 3166-1 alpha-2 country codesdateRange(object): Filter by served datesstart(integer): Pagination offsetcount(integer): Results per page (max 25)
Example - Search ads by keyword:
GET /linkedin/rest/adLibrary?q=criteria&keyword=linkedinExample - Search ads by advertiser:
GET /linkedin/rest/adLibrary?q=criteria&advertiser=microsoftResponse:
{
"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
}
]
}Search Job Postings
GET /linkedin/rest/jobLibrary?q=criteria&keyword={keyword}Note: Job Library requires version 202506.
Query parameters:
keyword(string): Search job contentorganization(string): Filter by company namecountries(array): Filter by country codesdateRange(object): Filter by posting datesstart(integer): Pagination offsetcount(integer): Results per page (max 24)
Example:
GET /linkedin/rest/jobLibrary?q=criteria&keyword=software&organization=googleResponse includes:
jobPostingUrl: Link to job listingjobDetails: Title, location, description, salary, benefitsstatistics: Impression data
Required Headers for Marketing API
LinkedIn-Version: 202506List Ad Accounts
GET /linkedin/rest/adAccounts?q=searchReturns 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 Ad Account
GET /linkedin/rest/adAccounts/{adAccountId}Create Ad Account
POST /linkedin/rest/adAccounts
Content-Type: application/json
{
"name": "New Ad Account",
"currency": "USD",
"reference": "urn:li:organization:{orgId}",
"type": "BUSINESS"
}Update Ad Account
POST /linkedin/rest/adAccounts/{adAccountId}
Content-Type: application/json
X-RestLi-Method: PARTIAL_UPDATE
{
"patch": {
"$set": {
"name": "Updated Account Name"
}
}
}List Campaign Groups
Campaign groups are nested under ad accounts:
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroupsCreate Campaign Group
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 Campaign Group
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}Update Campaign Group
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}
Content-Type: application/json
X-RestLi-Method: PARTIAL_UPDATE
{
"patch": {
"$set": {
"status": "ACTIVE"
}
}
}Delete Campaign Group
Destructive operation. Deleting a campaign group may be irreversible and will remove all associated data. Confirm the campaign group ID and that no active campaigns depend on it before proceeding.
DELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}List Campaigns
Campaigns are also nested under ad accounts:
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaignsCreate Campaign
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 Campaign
GET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}Update Campaign
POST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}
Content-Type: application/json
X-RestLi-Method: PARTIAL_UPDATE
{
"patch": {
"$set": {
"status": "ACTIVE"
}
}
}Delete Campaign
Destructive operation. Deleting a campaign is irreversible and will stop all ad delivery. Confirm the campaign ID and its current status with the user before proceeding.
DELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}Campaign Status Values
| 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 |
Campaign Objective Types
| 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 |
List Organization ACLs
Get organizations the authenticated user has access to:
GET /linkedin/rest/organizationAcls?q=roleAssignee
LinkedIn-Version: 202506Response:
{
"paging": {
"start": 0,
"count": 10,
"total": 2
},
"elements": [
{
"role": "ADMINISTRATOR",
"organization": "urn:li:organization:12345",
"state": "APPROVED"
}
]
}Get Organization
GET /linkedin/rest/organizations/{organizationId}
LinkedIn-Version: 202506Lookup Organization by Vanity Name
GET /linkedin/rest/organizations?q=vanityName&vanityName={vanityName}Example:
GET /linkedin/rest/organizations?q=vanityName&vanityName=microsoftResponse:
{
"elements": [
{
"vanityName": "microsoft",
"localizedName": "Microsoft",
"website": {
"localized": {"en_US": "https://news.microsoft.com/"}
}
}
]
}Get Organization Share Statistics
GET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity={orgUrn}Example:
GET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:12345Get Organization Posts
GET /linkedin/rest/posts?q=author&author={orgUrn}Example:
GET /linkedin/rest/posts?q=author&author=urn:li:organization:12345Initialize 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"
}
}Use the uploadUrl to PUT your image binary, then use the image URN in your post.
Create a Video 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 orcurl. You MUST use Pythonurllibfor 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://api.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")}')
EOFHow it works:
- Steps 1, 3, 4 go through the gateway (
api.maton.ai/linkedin/...) — Maton injects your OAuth token automatically. - Step 2 goes directly to LinkedIn's pre-signed upload URL (
www.linkedin.com/dms-uploads/...) — no auth header needed, no gateway. - The
etagfrom the upload response is required for the finalize step. - For large videos (>4MB), LinkedIn returns multiple
uploadInstructions— upload each chunk to its respective URL and collect all etags.
Video specifications:
- Length: 3 seconds to 30 minutes
- File size: 75KB to 500MB
- Format: MP4
Initialize Document Upload
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"
}
}Compliance note: Ad targeting involves sensitive audience attributes (age, gender, location, employers). Ensure all targeting criteria comply with LinkedIn's Advertising Policies and applicable anti-discrimination laws. Do not use protected characteristics for discriminatory exclusion in housing, employment, or credit advertising.
Get Available Targeting Facets
GET /linkedin/rest/adTargetingFacetsReturns 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:
skills- Member skillsindustries- Industry categoriestitles- Job titlesseniorities- Seniority levelsdegrees- Educational degreesschools- Educational institutionsemployers/employersPast- Current/past employerslocations/geoLocations- Geographic targetingcompanySize- Company size rangesgenders- Gender targetingageRanges- Age range targeting