A step-by-step guide to using the LinkedIn API and a bash script to publish posts — including images — directly from your terminal.
curl (already installed on Mac)https://localhostVisit this URL in your browser (replace YOUR_CLIENT_ID):
https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https://localhost&scope=w_member_social%20openid%20profile
Log in and approve. Your browser will redirect to https://localhost/?code=LONG_STRING. Copy the value after code=.
Run this quickly — the code expires in minutes:
curl -X POST 'https://www.linkedin.com/oauth/v2/accessToken' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=YOUR_AUTHORIZATION_CODE' \
--data-urlencode 'client_id=YOUR_CLIENT_ID' \
--data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
--data-urlencode 'redirect_uri=https://localhost'
Save the access_token from the response — it's valid for ~60 days.
curl -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' \
'https://api.linkedin.com/v2/userinfo'
Look for the sub field in the response — that's your Person ID.
Save this as linkedin-post.sh and fill in your TOKEN and PERSON_ID at the top:
#!/bin/bash
# Usage (text only): ./linkedin-post.sh "Your caption"
# Usage (with image): ./linkedin-post.sh "Your caption" /path/to/image.jpg
TOKEN="YOUR_ACCESS_TOKEN"
PERSON_ID="YOUR_PERSON_ID"
CAPTION="$1"
IMAGE_PATH="$2"
if [ -z "$CAPTION" ]; then
echo "Usage: ./linkedin-post.sh \"Your caption\" [/path/to/image.jpg]"
exit 1
fi
CAPTION_JSON=$(echo "$CAPTION" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read().strip()))')
if [ -n "$IMAGE_PATH" ]; then
REGISTER_RESPONSE=$(curl -s -X POST \
'https://api.linkedin.com/v2/assets?action=registerUpload' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-Restli-Protocol-Version: 2.0.0' \
-d "{
\"registerUploadRequest\": {
\"recipes\": [\"urn:li:digitalmediaRecipe:feedshare-image\"],
\"owner\": \"urn:li:person:$PERSON_ID\",
\"serviceRelationships\": [{
\"relationshipType\": \"OWNER\",
\"identifier\": \"urn:li:userGeneratedContent\"
}]
}
}")
UPLOAD_URL=$(echo "$REGISTER_RESPONSE" | grep -o '"uploadUrl":"[^"]*"' | sed 's/"uploadUrl":"//;s/"//')
ASSET_URN=$(echo "$REGISTER_RESPONSE" | grep -o '"asset":"[^"]*"' | sed 's/"asset":"//;s/"//')
curl -s -X PUT "$UPLOAD_URL" \
-H "Authorization: Bearer $TOKEN" \
--upload-file "$IMAGE_PATH"
curl -s -X POST 'https://api.linkedin.com/v2/ugcPosts' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-Restli-Protocol-Version: 2.0.0' \
-d "{
\"author\": \"urn:li:person:$PERSON_ID\",
\"lifecycleState\": \"PUBLISHED\",
\"specificContent\": {
\"com.linkedin.ugc.ShareContent\": {
\"shareCommentary\": {\"text\": $CAPTION_JSON},
\"shareMediaCategory\": \"IMAGE\",
\"media\": [{\"status\": \"READY\", \"media\": \"$ASSET_URN\"}]
}
},
\"visibility\": {\"com.linkedin.ugc.MemberNetworkVisibility\": \"PUBLIC\"}
}"
else
curl -s -X POST 'https://api.linkedin.com/v2/ugcPosts' \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-H 'X-Restli-Protocol-Version: 2.0.0' \
-d "{
\"author\": \"urn:li:person:$PERSON_ID\",
\"lifecycleState\": \"PUBLISHED\",
\"specificContent\": {
\"com.linkedin.ugc.ShareContent\": {
\"shareCommentary\": {\"text\": $CAPTION_JSON},
\"shareMediaCategory\": \"NONE\"
}
},
\"visibility\": {\"com.linkedin.ugc.MemberNetworkVisibility\": \"PUBLIC\"}
}"
fi
Make it executable:
chmod +x linkedin-post.sh
# Text only
./linkedin-post.sh "Hello from the command line!"
# With image
./linkedin-post.sh "Hello from the command line!" ~/Desktop/photo.jpg
# Multi-paragraph post
./linkedin-post.sh $'Paragraph one.\n\nParagraph two.'
$'...' quoting with \n for line breaks and \' for apostrophes