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.
REST API (/rest/...) calls take a LinkedIn-Version header (YYYYMM, e.g. 202606). If you leave it out, Maton sends 202606. LinkedIn retires old monthly versions and returns HTTP 426 NONEXISTENT_VERSION for retired or future-dated ones, so use a recent month.
Reference
Get Current User Profile
GET /linkedin/rest/me
LinkedIn-Version: 202606Example:
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', '202606')
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: 202606
{
"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: 202606
{
"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: 202606
{
"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: 202606
{
"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: 202606Search 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 202606.
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: 202606List 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: 202606Response:
{
"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: 202606Lookup 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: 202606
{
"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': '202606',
'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: 202606
{
"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