{"skill":{"slug":"linkedin-api","displayName":"LinkedIn","summary":"LinkedIn API integration with managed OAuth. Share posts, manage profile, and access LinkedIn features. Use this skill when users want to share content on Li...","description":"---\nname: linkedin\ndescription: |\n  LinkedIn API integration with managed OAuth. Share posts, manage profile, and access LinkedIn features.\n  Use this skill when users want to share content on LinkedIn, get profile/organization information, or interact with LinkedIn's platform.\n  Advertising features (campaigns, ad accounts) require additional OAuth scopes — verify granted scopes before use.\n  For other third party apps, use the api-gateway skill (https://clawhub.ai/byungkyu/api-gateway).\n  Requires network access and valid Maton API key.\nmetadata:\n  author: maton\n  version: \"1.0\"\n  clawdbot:\n    emoji: 🧠\n    requires:\n      env:\n        - MATON_API_KEY\n---\n\n# LinkedIn\n\nAccess the LinkedIn API with managed OAuth authentication. Share posts, manage advertising campaigns, retrieve profile and organization information, upload media, and access the Ad Library.\n\n## Quick Start\n\n```bash\n# Get current user profile\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/linkedin/rest/me')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nreq.add_header('LinkedIn-Version', '202506')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n## Base URL\n\n```\nhttps://api.maton.ai/linkedin/rest/{resource}\n```\n\nMaton proxies requests to `api.linkedin.com` and automatically injects your OAuth token.\n\n## Authentication\n\nAll requests require the Maton API key in the Authorization header:\n\n```\nAuthorization: Bearer $MATON_API_KEY\n```\n\n**Environment Variable:** Set your API key as `MATON_API_KEY`:\n\n```bash\nexport MATON_API_KEY=\"YOUR_API_KEY\"\n```\n\n### Getting Your API Key\n\n1. Sign in or create an account at [maton.ai](https://maton.ai)\n2. Go to [maton.ai/settings](https://maton.ai/settings)\n3. Copy your API key\n\n### Required Headers\n\nLinkedIn REST API requires the version header:\n\n```\nLinkedIn-Version: 202506\n```\n\n## Connection Management\n\nManage your LinkedIn OAuth connections at `https://api.maton.ai`.\n\n### List Connections\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/connections?app=linkedin&status=ACTIVE')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n### Create Connection\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\ndata = json.dumps({'app': 'linkedin'}).encode()\nreq = urllib.request.Request('https://api.maton.ai/connections', data=data, method='POST')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nreq.add_header('Content-Type', 'application/json')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n### Get Connection\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/connections/{connection_id}')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n**Response:**\n```json\n{\n  \"connection\": {\n    \"connection_id\": \"{connection_id}\",\n    \"status\": \"ACTIVE\",\n    \"creation_time\": \"2026-02-07T08:00:24.372659Z\",\n    \"last_updated_time\": \"2026-02-07T08:05:16.609085Z\",\n    \"url\": \"https://connect.maton.ai/?session_token=...\",\n    \"app\": \"linkedin\",\n    \"metadata\": {}\n  }\n}\n```\n\nOpen the returned `url` in a browser to complete OAuth authorization.\n\n### Delete Connection\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/connections/{connection_id}', method='DELETE')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n### Specifying Connection\n\nIf you have multiple LinkedIn connections, specify which one to use with the `Maton-Connection` header:\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/linkedin/rest/me')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nreq.add_header('LinkedIn-Version', '202506')\nreq.add_header('Maton-Connection', '{connection_id}')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\nIf you have multiple connections, always include this header to ensure requests go to the intended account.\n\n## Security & Permissions\n\n- Access is scoped to posts, profiles, organizations, images, videos, and analytics within the connected LinkedIn account.\n- **All write operations require explicit user approval.** Before executing any create, update, or delete call:\n  1. State the exact operation (e.g., \"Create a DRAFT campaign in ad account 123456789\")\n  2. Show the user the request body or key parameters\n  3. Wait for explicit confirmation before sending the request\n  4. For destructive operations (DELETE), additionally confirm the resource cannot be recovered\n- Advertising operations (campaign creation, budget changes, account modifications) carry financial impact. Always confirm budget amounts and targeting criteria with the user before execution.\n\n## API Reference\n\n### Profile\n\n#### Get Current User Profile\n\n```bash\nGET /linkedin/rest/me\nLinkedIn-Version: 202506\n```\n\n**Example:**\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/linkedin/rest/me')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nreq.add_header('LinkedIn-Version', '202506')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n**Response:**\n```json\n{\n  \"firstName\": {\n    \"localized\": {\"en_US\": \"John\"},\n    \"preferredLocale\": {\"country\": \"US\", \"language\": \"en\"}\n  },\n  \"localizedFirstName\": \"John\",\n  \"lastName\": {\n    \"localized\": {\"en_US\": \"Doe\"},\n    \"preferredLocale\": {\"country\": \"US\", \"language\": \"en\"}\n  },\n  \"localizedLastName\": \"Doe\",\n  \"id\": \"yrZCpj2Z12\",\n  \"vanityName\": \"johndoe\",\n  \"localizedHeadline\": \"Software Engineer at Example Corp\",\n  \"profilePicture\": {\n    \"displayImage\": \"urn:li:digitalmediaAsset:C4D00AAAAbBCDEFGhiJ\"\n  }\n}\n```\n\n### Sharing Posts\n\n#### Create a Text Post\n\n```bash\nPOST /linkedin/rest/posts\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"author\": \"urn:li:person:{personId}\",\n  \"lifecycleState\": \"PUBLISHED\",\n  \"visibility\": \"PUBLIC\",\n  \"commentary\": \"Hello LinkedIn! This is my first API post.\",\n  \"distribution\": {\n    \"feedDistribution\": \"MAIN_FEED\"\n  }\n}\n```\n\n**Response:** `201 Created` with `x-restli-id` header containing the post URN.\n\n#### Create an Article/URL Share\n\n```bash\nPOST /linkedin/rest/posts\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"author\": \"urn:li:person:{personId}\",\n  \"lifecycleState\": \"PUBLISHED\",\n  \"visibility\": \"PUBLIC\",\n  \"commentary\": \"Check out this great article!\",\n  \"distribution\": {\n    \"feedDistribution\": \"MAIN_FEED\"\n  },\n  \"content\": {\n    \"article\": {\n      \"source\": \"https://example.com/article\",\n      \"title\": \"Article Title\",\n      \"description\": \"Article description here\"\n    }\n  }\n}\n```\n\n#### Create an Image Post\n\nFirst, initialize the image upload, then upload the image, then create the post.\n\n**Step 1: Initialize Image Upload**\n```bash\nPOST /linkedin/rest/images?action=initializeUpload\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"initializeUploadRequest\": {\n    \"owner\": \"urn:li:person:{personId}\"\n  }\n}\n```\n\n**Response:**\n```json\n{\n  \"value\": {\n    \"uploadUrlExpiresAt\": 1770541529250,\n    \"uploadUrl\": \"https://www.linkedin.com/dms-uploads/...\",\n    \"image\": \"urn:li:image:D4D10AQH4GJAjaFCkHQ\"\n  }\n}\n```\n\n**Step 2: Upload Image Binary**\n```bash\nPUT {uploadUrl from step 1}\nContent-Type: image/png\n\n{binary image data}\n```\n\n**Step 3: Create Image Post**\n```bash\nPOST /linkedin/rest/posts\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"author\": \"urn:li:person:{personId}\",\n  \"lifecycleState\": \"PUBLISHED\",\n  \"visibility\": \"PUBLIC\",\n  \"commentary\": \"Check out this image!\",\n  \"distribution\": {\n    \"feedDistribution\": \"MAIN_FEED\"\n  },\n  \"content\": {\n    \"media\": {\n      \"id\": \"urn:li:image:D4D10AQH4GJAjaFCkHQ\",\n      \"title\": \"Image Title\"\n    }\n  }\n}\n```\n\n### Visibility Options\n\n| Value | Description |\n|-------|-------------|\n| `PUBLIC` | Viewable by anyone on LinkedIn |\n| `CONNECTIONS` | Viewable by 1st-degree connections only |\n\n### Share Media Categories\n\n| Value | Description |\n|-------|-------------|\n| `NONE` | Text-only post |\n| `ARTICLE` | URL/article share |\n| `IMAGE` | Image post |\n| `VIDEO` | Video post |\n\n### Ad Library (Public Data)\n\nThe Ad Library API provides access to public advertising data on LinkedIn. These endpoints use the REST API with version headers.\n\n#### Required Headers for Ad Library\n\n```\nLinkedIn-Version: 202506\n```\n\n#### Search Ads\n\n```bash\nGET /linkedin/rest/adLibrary?q=criteria&keyword={keyword}\n```\n\nQuery parameters:\n- `keyword` (string): Search ad content (multiple keywords use AND logic)\n- `advertiser` (string): Search by advertiser name\n- `countries` (array): Filter by ISO 3166-1 alpha-2 country codes\n- `dateRange` (object): Filter by served dates\n- `start` (integer): Pagination offset\n- `count` (integer): Results per page (max 25)\n\n**Example - Search ads by keyword:**\n```bash\nGET /linkedin/rest/adLibrary?q=criteria&keyword=linkedin\n```\n\n**Example - Search ads by advertiser:**\n```bash\nGET /linkedin/rest/adLibrary?q=criteria&advertiser=microsoft\n```\n\n**Response:**\n```json\n{\n  \"paging\": {\n    \"start\": 0,\n    \"count\": 10,\n    \"total\": 11619543,\n    \"links\": [...]\n  },\n  \"elements\": [\n    {\n      \"adUrl\": \"https://www.linkedin.com/ad-library/detail/...\",\n      \"details\": {\n        \"advertiser\": {...},\n        \"adType\": \"TEXT_AD\",\n        \"targeting\": {...},\n        \"statistics\": {\n          \"firstImpressionDate\": 1704067200000,\n          \"latestImpressionDate\": 1706745600000,\n          \"impressionsFrom\": 1000,\n          \"impressionsTo\": 5000\n        }\n      },\n      \"isRestricted\": false\n    }\n  ]\n}\n```\n\n#### Search Job Postings\n\n```bash\nGET /linkedin/rest/jobLibrary?q=criteria&keyword={keyword}\n```\n\n**Note:** Job Library requires version `202506`.\n\nQuery parameters:\n- `keyword` (string): Search job content\n- `organization` (string): Filter by company name\n- `countries` (array): Filter by country codes\n- `dateRange` (object): Filter by posting dates\n- `start` (integer): Pagination offset\n- `count` (integer): Results per page (max 24)\n\n**Example:**\n```bash\nGET /linkedin/rest/jobLibrary?q=criteria&keyword=software&organization=google\n```\n\n**Response includes:**\n- `jobPostingUrl`: Link to job listing\n- `jobDetails`: Title, location, description, salary, benefits\n- `statistics`: Impression data\n\n### Marketing API (Advertising)\n\nThe Marketing API provides access to LinkedIn's advertising platform. These endpoints use the versioned REST API.\n\n#### Required Headers for Marketing API\n\n```\nLinkedIn-Version: 202506\n```\n\n#### List Ad Accounts\n\n```bash\nGET /linkedin/rest/adAccounts?q=search\n```\n\nReturns all ad accounts accessible by the authenticated user.\n\n**Response:**\n```json\n{\n  \"paging\": {\n    \"start\": 0,\n    \"count\": 10,\n    \"links\": []\n  },\n  \"elements\": [\n    {\n      \"id\": 123456789,\n      \"name\": \"My Ad Account\",\n      \"status\": \"ACTIVE\",\n      \"type\": \"BUSINESS\",\n      \"currency\": \"USD\",\n      \"reference\": \"urn:li:organization:12345\"\n    }\n  ]\n}\n```\n\n#### Get Ad Account\n\n```bash\nGET /linkedin/rest/adAccounts/{adAccountId}\n```\n\n#### Create Ad Account\n\n```bash\nPOST /linkedin/rest/adAccounts\nContent-Type: application/json\n\n{\n  \"name\": \"New Ad Account\",\n  \"currency\": \"USD\",\n  \"reference\": \"urn:li:organization:{orgId}\",\n  \"type\": \"BUSINESS\"\n}\n```\n\n#### Update Ad Account\n\n```bash\nPOST /linkedin/rest/adAccounts/{adAccountId}\nContent-Type: application/json\nX-RestLi-Method: PARTIAL_UPDATE\n\n{\n  \"patch\": {\n    \"$set\": {\n      \"name\": \"Updated Account Name\"\n    }\n  }\n}\n```\n\n#### List Campaign Groups\n\nCampaign groups are nested under ad accounts:\n\n```bash\nGET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups\n```\n\n#### Create Campaign Group\n\n```bash\nPOST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups\nContent-Type: application/json\n\n{\n  \"name\": \"Q1 2026 Campaigns\",\n  \"status\": \"DRAFT\",\n  \"runSchedule\": {\n    \"start\": 1704067200000,\n    \"end\": 1711929600000\n  },\n  \"totalBudget\": {\n    \"amount\": \"10000\",\n    \"currencyCode\": \"USD\"\n  }\n}\n```\n\n#### Get Campaign Group\n\n```bash\nGET /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}\n```\n\n#### Update Campaign Group\n\n```bash\nPOST /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}\nContent-Type: application/json\nX-RestLi-Method: PARTIAL_UPDATE\n\n{\n  \"patch\": {\n    \"$set\": {\n      \"status\": \"ACTIVE\"\n    }\n  }\n}\n```\n\n#### Delete Campaign Group\n\n> **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.\n\n```bash\nDELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaignGroups/{campaignGroupId}\n```\n\n#### List Campaigns\n\nCampaigns are also nested under ad accounts:\n\n```bash\nGET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns\n```\n\n#### Create Campaign\n\n```bash\nPOST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns\nContent-Type: application/json\n\n{\n  \"campaignGroup\": \"urn:li:sponsoredCampaignGroup:123456\",\n  \"name\": \"Brand Awareness Campaign\",\n  \"status\": \"DRAFT\",\n  \"type\": \"SPONSORED_UPDATES\",\n  \"objectiveType\": \"BRAND_AWARENESS\",\n  \"dailyBudget\": {\n    \"amount\": \"100\",\n    \"currencyCode\": \"USD\"\n  },\n  \"costType\": \"CPM\",\n  \"unitCost\": {\n    \"amount\": \"5\",\n    \"currencyCode\": \"USD\"\n  },\n  \"locale\": {\n    \"country\": \"US\",\n    \"language\": \"en\"\n  }\n}\n```\n\n#### Get Campaign\n\n```bash\nGET /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}\n```\n\n#### Update Campaign\n\n```bash\nPOST /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}\nContent-Type: application/json\nX-RestLi-Method: PARTIAL_UPDATE\n\n{\n  \"patch\": {\n    \"$set\": {\n      \"status\": \"ACTIVE\"\n    }\n  }\n}\n```\n\n#### Delete Campaign\n\n> **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.\n\n```bash\nDELETE /linkedin/rest/adAccounts/{adAccountId}/adCampaigns/{campaignId}\n```\n\n### Campaign Status Values\n\n| Status | Description |\n|--------|-------------|\n| `DRAFT` | Campaign is in draft mode |\n| `ACTIVE` | Campaign is running |\n| `PAUSED` | Campaign is paused |\n| `ARCHIVED` | Campaign is archived |\n| `COMPLETED` | Campaign has ended |\n| `CANCELED` | Campaign was canceled |\n\n### Campaign Objective Types\n\n| Objective | Description |\n|-----------|-------------|\n| `BRAND_AWARENESS` | Increase brand visibility |\n| `WEBSITE_VISITS` | Drive traffic to website |\n| `ENGAGEMENT` | Increase post engagement |\n| `VIDEO_VIEWS` | Maximize video views |\n| `LEAD_GENERATION` | Collect leads via Lead Gen Forms |\n| `WEBSITE_CONVERSIONS` | Drive website conversions |\n| `JOB_APPLICANTS` | Attract job applications |\n\n### Organizations\n\n#### List Organization ACLs\n\nGet organizations the authenticated user has access to:\n\n```bash\nGET /linkedin/rest/organizationAcls?q=roleAssignee\nLinkedIn-Version: 202506\n```\n\n**Response:**\n```json\n{\n  \"paging\": {\n    \"start\": 0,\n    \"count\": 10,\n    \"total\": 2\n  },\n  \"elements\": [\n    {\n      \"role\": \"ADMINISTRATOR\",\n      \"organization\": \"urn:li:organization:12345\",\n      \"state\": \"APPROVED\"\n    }\n  ]\n}\n```\n\n#### Get Organization\n\n```bash\nGET /linkedin/rest/organizations/{organizationId}\nLinkedIn-Version: 202506\n```\n\n#### Lookup Organization by Vanity Name\n\n```bash\nGET /linkedin/rest/organizations?q=vanityName&vanityName={vanityName}\n```\n\n**Example:**\n```bash\nGET /linkedin/rest/organizations?q=vanityName&vanityName=microsoft\n```\n\n**Response:**\n```json\n{\n  \"elements\": [\n    {\n      \"vanityName\": \"microsoft\",\n      \"localizedName\": \"Microsoft\",\n      \"website\": {\n        \"localized\": {\"en_US\": \"https://news.microsoft.com/\"}\n      }\n    }\n  ]\n}\n```\n\n#### Get Organization Share Statistics\n\n```bash\nGET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity={orgUrn}\n```\n\n**Example:**\n```bash\nGET /linkedin/rest/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:12345\n```\n\n#### Get Organization Posts\n\n```bash\nGET /linkedin/rest/posts?q=author&author={orgUrn}\n```\n\n**Example:**\n```bash\nGET /linkedin/rest/posts?q=author&author=urn:li:organization:12345\n```\n\n### Media Upload (REST API)\n\nThe REST API provides modern media upload endpoints. All require version header `LinkedIn-Version: 202506`.\n\n#### Initialize Image Upload\n\n```bash\nPOST /linkedin/rest/images?action=initializeUpload\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"initializeUploadRequest\": {\n    \"owner\": \"urn:li:person:{personId}\"\n  }\n}\n```\n\n**Response:**\n```json\n{\n  \"value\": {\n    \"uploadUrlExpiresAt\": 1770541529250,\n    \"uploadUrl\": \"https://www.linkedin.com/dms-uploads/...\",\n    \"image\": \"urn:li:image:D4D10AQH4GJAjaFCkHQ\"\n  }\n}\n```\n\nUse the `uploadUrl` to PUT your image binary, then use the `image` URN in your post.\n\n#### Create a Video Post\n\nVideo uploads are a 4-step process: initialize, upload binary, finalize, then create the post.\n\n> **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.\n\n**Complete working example:**\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\n\nGATEWAY = 'https://api.maton.ai'\nHEADERS = {\n    'Authorization': f'Bearer {os.environ[\"MATON_API_KEY\"]}',\n    'Content-Type': 'application/json',\n    'LinkedIn-Version': '202506',\n    'X-Restli-Protocol-Version': '2.0.0',\n}\n\n# Step 0: Get person ID\nreq = urllib.request.Request(f'{GATEWAY}/linkedin/rest/me')\nfor k, v in HEADERS.items(): req.add_header(k, v)\nperson_id = json.load(urllib.request.urlopen(req))['id']\nowner = f'urn:li:person:{person_id}'\n\n# Step 1: Initialize upload (via gateway)\nfile_path = '/path/to/video.mp4'\nfile_size = os.path.getsize(file_path)\n\ninit_data = json.dumps({\n    'initializeUploadRequest': {\n        'owner': owner,\n        'fileSizeBytes': file_size,\n        'uploadCaptions': False,\n        'uploadThumbnail': False,\n    }\n}).encode()\n\nreq = urllib.request.Request(f'{GATEWAY}/linkedin/rest/videos?action=initializeUpload', data=init_data, method='POST')\nfor k, v in HEADERS.items(): req.add_header(k, v)\ninit_resp = json.load(urllib.request.urlopen(req))\nupload_url = init_resp['value']['uploadInstructions'][0]['uploadUrl']\nvideo_urn = init_resp['value']['video']\n\n# Step 2: Upload binary DIRECTLY to LinkedIn's pre-signed URL (NOT through the gateway)\n# The upload URL points to www.linkedin.com — it is pre-signed and needs NO Authorization header.\n# IMPORTANT: Use the URL exactly as returned by json.load() — do NOT pass it through shell variables.\nwith open(file_path, 'rb') as f:\n    video_data = f.read()\n\nupload_req = urllib.request.Request(upload_url, data=video_data, method='PUT')\nupload_req.add_header('Content-Type', 'application/octet-stream')\nupload_resp = urllib.request.urlopen(upload_req)\netag = upload_resp.headers['etag']\n\n# Step 3: Finalize upload (via gateway)\nfinalize_data = json.dumps({\n    'finalizeUploadRequest': {\n        'video': video_urn,\n        'uploadToken': '',\n        'uploadedPartIds': [etag],\n    }\n}).encode()\n\nreq = urllib.request.Request(f'{GATEWAY}/linkedin/rest/videos?action=finalizeUpload', data=finalize_data, method='POST')\nfor k, v in HEADERS.items(): req.add_header(k, v)\nurllib.request.urlopen(req)\n\n# Step 4: Create post with video (via gateway)\npost_data = json.dumps({\n    'author': owner,\n    'lifecycleState': 'PUBLISHED',\n    'visibility': 'PUBLIC',\n    'commentary': 'Check out this video!',\n    'distribution': {'feedDistribution': 'MAIN_FEED'},\n    'content': {'media': {'id': video_urn}},\n}).encode()\n\nreq = urllib.request.Request(f'{GATEWAY}/linkedin/rest/posts', data=post_data, method='POST')\nfor k, v in HEADERS.items(): req.add_header(k, v)\nresp = urllib.request.urlopen(req)\nprint(f'Video post created! {resp.headers.get(\"location\")}')\nEOF\n```\n\n**How it works:**\n- Steps 1, 3, 4 go through the gateway (`api.maton.ai/linkedin/...`) — Maton injects your OAuth token automatically.\n- Step 2 goes **directly** to LinkedIn's pre-signed upload URL (`www.linkedin.com/dms-uploads/...`) — no auth header needed, no gateway.\n- The `etag` from the upload response is required for the finalize step.\n- For large videos (>4MB), LinkedIn returns multiple `uploadInstructions` — upload each chunk to its respective URL and collect all etags.\n\n**Video specifications:**\n- Length: 3 seconds to 30 minutes\n- File size: 75KB to 500MB\n- Format: MP4\n\n#### Initialize Document Upload\n\n```bash\nPOST /linkedin/rest/documents?action=initializeUpload\nContent-Type: application/json\nLinkedIn-Version: 202506\n\n{\n  \"initializeUploadRequest\": {\n    \"owner\": \"urn:li:person:{personId}\"\n  }\n}\n```\n\n**Response:**\n```json\n{\n  \"value\": {\n    \"uploadUrlExpiresAt\": 1770541530896,\n    \"uploadUrl\": \"https://www.linkedin.com/dms-uploads/...\",\n    \"document\": \"urn:li:document:D4D10AQHr-e30QZCAjQ\"\n  }\n}\n```\n\n### Ad Targeting\n\n> **Compliance note:** Ad targeting involves sensitive audience attributes (age, gender, location, employers). Ensure all targeting criteria comply with LinkedIn's [Advertising Policies](https://www.linkedin.com/legal/ads-policy) and applicable anti-discrimination laws. Do not use protected characteristics for discriminatory exclusion in housing, employment, or credit advertising.\n\n#### Get Available Targeting Facets\n\n```bash\nGET /linkedin/rest/adTargetingFacets\n```\n\nReturns all available targeting facets for ad campaigns (31 facets including employers, degrees, skills, locations, industries, etc.).\n\n**Response:**\n```json\n{\n  \"elements\": [\n    {\n      \"facetName\": \"skills\",\n      \"adTargetingFacetUrn\": \"urn:li:adTargetingFacet:skills\",\n      \"entityTypes\": [\"SKILL\"],\n      \"availableEntityFinders\": [\"AD_TARGETING_FACET\", \"TYPEAHEAD\"]\n    },\n    {\n      \"facetName\": \"industries\",\n      \"adTargetingFacetUrn\": \"urn:li:adTargetingFacet:industries\"\n    }\n  ]\n}\n```\n\nAvailable targeting facets include:\n- `skills` - Member skills\n- `industries` - Industry categories\n- `titles` - Job titles\n- `seniorities` - Seniority levels\n- `degrees` - Educational degrees\n- `schools` - Educational institutions\n- `employers` / `employersPast` - Current/past employers\n- `locations` / `geoLocations` - Geographic targeting\n- `companySize` - Company size ranges\n- `genders` - Gender targeting\n- `ageRanges` - Age range targeting\n\n## Getting Your Person ID\n\nTo create posts, you need your LinkedIn person ID. Get it from the `/rest/me` endpoint:\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/linkedin/rest/me')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nreq.add_header('LinkedIn-Version', '202506')\nresult = json.load(urllib.request.urlopen(req))\nprint(f\"Your person URN: urn:li:person:{result['id']}\")\nEOF\n```\n\n## Code Examples\n\n### JavaScript - Create Text Post\n\n```javascript\nconst personId = 'YOUR_PERSON_ID';\n\nconst response = await fetch(\n  'https://api.maton.ai/linkedin/rest/posts',\n  {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${process.env.MATON_API_KEY}`,\n      'Content-Type': 'application/json',\n      'LinkedIn-Version': '202506'\n    },\n    body: JSON.stringify({\n      author: `urn:li:person:${personId}`,\n      lifecycleState: 'PUBLISHED',\n      visibility: 'PUBLIC',\n      commentary: 'Hello from the API!',\n      distribution: {\n        feedDistribution: 'MAIN_FEED'\n      }\n    })\n  }\n);\n```\n\n### Python - Create Text Post\n\n```python\nimport os\nimport requests\n\nperson_id = 'YOUR_PERSON_ID'\n\nresponse = requests.post(\n    'https://api.maton.ai/linkedin/rest/posts',\n    headers={\n        'Authorization': f'Bearer {os.environ[\"MATON_API_KEY\"]}',\n        'Content-Type': 'application/json',\n        'LinkedIn-Version': '202506'\n    },\n    json={\n        'author': f'urn:li:person:{person_id}',\n        'lifecycleState': 'PUBLISHED',\n        'visibility': 'PUBLIC',\n        'commentary': 'Hello from the API!',\n        'distribution': {\n            'feedDistribution': 'MAIN_FEED'\n        }\n    }\n)\n```\n\n## Rate Limits\n\n| Throttle Type | Daily Limit (UTC) |\n|---------------|-------------------|\n| Member | 150 requests/day |\n| Application | 100,000 requests/day |\n\n## Little Text Format (Commentary Field)\n\nThe `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.**\n\n### Reserved Characters (Must Escape)\n\n| Character | Escape As |\n|-----------|-----------|\n| `\\` | `\\\\` |\n| `\\|` | `\\\\|` |\n| `{` | `\\{` |\n| `}` | `\\}` |\n| `@` | `\\@` |\n| `[` | `\\[` |\n| `]` | `\\]` |\n| `(` | `\\(` |\n| `)` | `\\)` |\n| `<` | `\\<` |\n| `>` | `\\>` |\n| `#` | `\\#` |\n| `*` | `\\*` |\n| `_` | `\\_` |\n| `~` | `\\~` |\n\n### Example\n\n```json\n{\n  \"commentary\": \"Hello\\\\! Check out these bullet points:\\\\n\\\\n\\\\* Point 1\\\\n\\\\* Point 2\\\\n\\\\* More info \\\\(details inside\\\\)\"\n}\n```\n\n### Mentions and Hashtags\n\nUse Little Text Format syntax for mentions and hashtags:\n\n- **Mention a person:** `@[Display Name](urn:li:person:123)`\n- **Mention an organization:** `@[Company Name](urn:li:organization:456)`\n- **Hashtag (template):** `{hashtag|\\\\#|MyTag}`\n- **Hashtag (simple):** `#hashtag` (single words only)\n\n### Python Helper Function\n\n```python\ndef escape_linkedin_commentary(text):\n    \"\"\"Escape reserved characters for LinkedIn Little Text Format.\"\"\"\n    reserved = ['\\\\', '|', '{', '}', '@', '[', ']', '(', ')', '<', '>', '#', '*', '_', '~']\n    for char in reserved:\n        text = text.replace(char, '\\\\' + char)\n    return text\n\n# Usage\ncommentary = escape_linkedin_commentary(\"Check this out! Details (inside) #tech\")\n# Result: \"Check this out\\\\! Details \\\\(inside\\\\) \\\\#tech\"\n```\n\n## Notes\n\n- Person IDs are unique per application and not transferable across apps\n- **Commentary uses Little Text Format** — escape reserved characters (`\\|{}@[]()<>#*_~`) with backslash or content will be truncated\n- The `author` field must use URN format: `urn:li:person:{personId}`\n- All posts require `lifecycleState: \"PUBLISHED\"`\n- Image uploads are a 3-step process: initialize, upload binary, create post\n- Video uploads are a 4-step process: initialize, upload binary, finalize, create post\n- **Media upload URLs (images, videos, documents) point to `www.linkedin.com`, NOT `api.linkedin.com`.** These are pre-signed URLs that do NOT go through the gateway and do NOT require an Authorization header. You MUST use Python `urllib` to handle these URLs — do NOT pass them through shell variables or use `curl`, as the URL contains encoded characters (`%253D`) that get corrupted by shell expansion.\n- Include `LinkedIn-Version: 202506` header for all REST API calls\n- Profile picture URLs may expire; re-fetch if needed\n\n## Error Handling\n\n| Status | Meaning |\n|--------|---------|\n| 400 | Missing LinkedIn connection or invalid request |\n| 401 | Invalid or missing Maton API key |\n| 403 | Insufficient permissions (check OAuth scopes) |\n| 404 | Resource not found |\n| 422 | Invalid request body or URN format |\n| 429 | Rate limited |\n| 4xx/5xx | Passthrough error from LinkedIn API |\n\n### Error Response Format\n\n```json\n{\n  \"status\": 403,\n  \"serviceErrorCode\": 100,\n  \"code\": \"ACCESS_DENIED\",\n  \"message\": \"Not enough permissions to access resource\"\n}\n```\n\n### Troubleshooting: API Key Issues\n\n1. Check that the `MATON_API_KEY` environment variable is set:\n\n```bash\necho $MATON_API_KEY\n```\n\n2. Verify the API key is valid by listing connections:\n\n```bash\npython <<'EOF'\nimport urllib.request, os, json\nreq = urllib.request.Request('https://api.maton.ai/connections')\nreq.add_header('Authorization', f'Bearer {os.environ[\"MATON_API_KEY\"]}')\nprint(json.dumps(json.load(urllib.request.urlopen(req)), indent=2))\nEOF\n```\n\n### Troubleshooting: Invalid App Name\n\n1. Ensure your URL path starts with `linkedin`. For example:\n\n- Correct: `https://api.maton.ai/linkedin/rest/me`\n- Incorrect: `https://api.maton.ai/rest/me`\n\n## OAuth Scopes\n\n| Scope | Description |\n|-------|-------------|\n| `openid` | OpenID Connect authentication |\n| `profile` | Read basic profile |\n| `email` | Read email address |\n| `w_member_social` | Create, modify, and delete posts |\n| `r_organization_social` | Read organization posts and statistics |\n| `w_organization_social` | Create and manage organization posts |\n| `r_ads` | Read advertising account data |\n| `rw_ads` | Create and manage ad campaigns, campaign groups, and accounts |\n\nNote: Available scopes depend on your LinkedIn OAuth connection. Verify granted scopes at your [Maton connection settings](https://maton.ai/settings) before attempting advertising operations.\n\n## Resources\n\n- [LinkedIn API Overview](https://learn.microsoft.com/en-us/linkedin/)\n- [Share on LinkedIn Guide](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin)\n- [Profile API](https://learn.microsoft.com/en-us/linkedin/shared/integrations/people/profile-api)\n- [Sign In with LinkedIn](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2)\n- [Authentication Guide](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authentication)\n- [Marketing API](https://learn.microsoft.com/en-us/linkedin/marketing/)\n- [Ad Accounts](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-accounts)\n- [Campaign Management](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-campaigns)\n- [Ad Library API](https://www.linkedin.com/ad-library/api/)\n- [Maton Community](https://discord.com/invite/dBfFAcefs2)\n- [Maton Support](mailto:support@maton.ai)\n","tags":{"latest":"1.0.9"},"stats":{"comments":0,"downloads":11443,"installsAllTime":37,"installsCurrent":37,"stars":45,"versions":10},"createdAt":1770462910278,"updatedAt":1781739757511},"latestVersion":{"version":"1.0.9","createdAt":1780655464060,"changelog":"- Removed the skill-card.md file from the repository.\n- Documentation updated: clarified that advertising features (campaigns, ad accounts) require additional OAuth scopes and users should verify their granted scopes before use.\n- Enhanced security and permissions guidelines to include explicit multi-step user confirmation for write operations, especially for advertising calls that carry financial impact.\n- Updated the description to reflect new requirements regarding advertising features and OAuth scopes.","license":"MIT-0"},"metadata":{"setup":[{"key":"MATON_API_KEY","required":true}],"os":null,"systems":null},"owner":{"handle":"byungkyu","userId":"s174c3s2kc1ehqj1ytzntezj2983e2aj","displayName":"byungkyu","image":"https://avatars.githubusercontent.com/u/16563684?v=4"},"moderation":null}