LinkedIn API Tutorial: Posting Images and Text

Last week I was jumping from one tap to another, so I could post on LinkedIn from Time Logger an application I'm building, even the documentation from Microsoft was outdated. After so many StackOverflow questions and ChatGPT prompts, it worked.
So here is how to post on LinkedIn using the API as of now.

Getting the User's Access Token.

I assume you already have your client key and secret, and have the "Share on LinkedIn" and "Sign In with LinkedIn using OpenID Connect" products. If not, you can quickly create a new app here. You’ll also need a company set up with LinkedIn, which is quite straightforward.

Get Your App Authenticated

You'll need the user to authenticate your app from the authentication request URL it should be as follows:
https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=<YOUR_CLIENT_ID>&redirect_uri=<REDIRECT_URI>&state=<RANDOM_NUMBER>&scope=<SPACE_SEPARATED_SCOPE_NAMES>

<*YOUR_CLIENT_ID*> you can find it in your app's auth tab.
<*REDIRECT_URI*> you should set this to one of the URIs you added to your app in the auth tab as well, the user will be redirected to this URI once they authenticate your app with two parameters code, and state (if you set it).
<*RANDOM_NUMBER*> not required. It's set to prevent CSRF, you can use it to make sure the authentication request was sent from your app.
<*SPACE_SEPARATED_SCOPE_NAMES*> use %20 Instead of spaces, if you're hard coding the URL, you can find the scope names in your app's auth tab.

Example:
https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=SKLJE898&redirect_uri=http://localhost:7878/linkedin/&scope=w_member_social%20openid%20email%20profile

Request Access Token

To get the access token you need to send a request to this endpoint https://www.linkedin.com/oauth/v2/accessToken with the following request body:

{
     "grant_type": "authorization_code",
     "code": "CODE",
     "redirect_uri": "<REDIRECT_URI>",
     "client_id": "<CLIENT_ID>",
     "client_secret": "<CLIENT_SECRET>"
}

Here is Python code of how you might do it:

import requests
import uvicorn
import webbrowser
from fastapi import FastAPI
from starlette.responses import HTMLResponse


asgi_app = FastAPI()
client_id = "your-client-id"
client_secret = "your-client-secret"

webbrowser.open("https://www.linkedin.com/oauth/v2/authorization?response_type=code&"
                f"client_id={client_id}&redirect_uri"
                "=http://localhost:7878/linkedin/&scope=w_member_social%20openid%20email%20profile")

@asgi_app.get("/linkedin/")
def linkedin_login(code):
    try:
        url = 'https://www.linkedin.com/oauth/v2/accessToken'
        params = {
            'grant_type': 'authorization_code',
            'code': code,
            'redirect_uri': 'http://localhost:7878/linkedin/',
            'client_id': client_id,
            'client_secret': client_secret
        }
        response = requests.post(url, data=params)

        if response.status_code == 200:
            access_token = response.json()['access_token']
            print(access_token)
            # redirect the user to your app or just
            return HTMLResponse("Return to to application") 
        else: 
            return HTMLResponse(f'{response.status_code}: {response.text}')
    except Exception as e:
        print(e)
        return HTMLResponse(e)

if __name__ == "__main__":
    uvicorn.run(asgi_app, host="127.0.0.1", port=7878)

Get User URN

The user URN will look something like this urn:li:person:uh837IJ, you'll need it to take actions on behalf of the user, in order to get it, make sure you had the user authenticate your app with the profile scope.

This time you'll send a request to this endpoint https://api.linkedin.com/v2/userinfo with the following header: {"Authorization": f"Bearer <ACCESS_TOKEN>"}

Here is Python code of how you might do it:

import requests
access_token = 'user-access-token'

url = "https://api.linkedin.com/v2/userinfo"
headers = {"Authorization": f"Bearer {access_token}"}
try:
    response = requests.get(url, headers=headers)
    if 300 > response.status_code >= 200:
        user_data = response.json()
        user_id = user_data.get("sub")
        user_urn = f"urn:li:person:{user_id}" 
    else: 
        print(f'{response.status_code}: {response.text}')

except Exception as e:
    print(e)

Posting Plain Text

The endpoint for posting is https://api.linkedin.com/v2/ugcPosts
The headers are:

{
"Authorization": "Bearer <ACCESS_TOKEN>",
"Connection": "Keep-Alive",
"Linkedin-Version": "format AAAAMM",
"X-Restli-Protocol-Version": "2.0.0",
"Content-Type": "application/json"
}

The body should be as follows:

{
    "author": "<USER_URN>",
    "lifecycleState": "PUBLISHED",
    "specificContent": {
        "com.linkedin.ugc.ShareContent": {
            "shareCommentary": {
                "text": "<YOUR_POST>"
            },
            "shareMediaCategory": "NONE"
        }
    },
    "visibility":  {
        "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
    }
}

Here is Python code of how you might do it:

import requests
access_token = "user-access-token"
user_urn = "user-urn"
content = "LinkedIn API post test"

api_url = 'https://api.linkedin.com/v2/ugcPosts'

headers = {
    'Authorization': f'Bearer {access_token}',
    'Connection': 'Keep-Alive',
    'Linkedin-Version': "format AAAAMM",
    "X-Restli-Protocol-Version": '2.0.0',
    'Content-Type': 'application/json',
}

post_body = {
    "author": user_urn,
    "lifecycleState": "PUBLISHED",
    "specificContent": {
        "com.linkedin.ugc.ShareContent": {
            "shareCommentary": {
                "text": content
            },
            "shareMediaCategory": "NONE"
        }
    },
    "visibility":  {
        "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
    }
}

response = requests.post(api_url, headers=headers, json=post_body)
if response.status_code == 201:
    print("posted successfully", response.text)
else:
    print(f'{response.status_code}: {response.text}')

Posting images

Upload Images

before you can post an image, you have to register and upload it, meaning you'll have to send two requests.

Register Image

The first to this URL https://api.linkedin.com/v2/assets after adding action parameter set to registerUpload and oauth2_access_token set to your user's access token it should look like this:
https://api.linkedin.com/v2/assets?action=registerUpload&oauth2_access_token=<ACCESS_TOKEN> with this body:

{
    "registerUploadRequest": {
        "owner": "<USER_URN>",
        "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
        "serviceRelationships": [
            {
                "identifier": "urn:li:userGeneratedContent",
                "relationshipType": "OWNER"
            }
        ]
    }
}

The response should be a JSON containing an uploadUrl to upload to and URN for the asset

Upload Image

The second is the uploadUrl you get from the first request, the body should be the binary of the image file the headers should be as follows:

{
    "Authorization": "Bearer <ACCESS_TOKEN>",
    "Content-Type": "application/octet-stream"
}

Here is Python code of how you might do it:

import requests
from os.path import exists
access_token = "user-access-token"
user_urn = "user-urn"

img_paths = ['list', 'of', 'images', 'paths']
images_ids = []

upload_headers = {
    "Authorization": f"Bearer {access_token}",
    "Content-Type": "application/octet-stream"
}

for image_file_path in img_paths:
    if exists(image_file_path):
        try:
            payload = {
                "registerUploadRequest": {
                    "owner": user_urn,
                    "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
                    "serviceRelationships": [
                        {
                            "identifier": "urn:li:userGeneratedContent",
                            "relationshipType": "OWNER"
                        }
                    ]
                }
            }
            register_post = requests.post(
                f"https://api.linkedin.com/v2/assets?action=registerUpload&oauth2_access_token={access_token}",
                json=payload
            ).json()

            upload_url = register_post['value']['uploadMechanism'][
                'com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl']

            requests.post(upload_url, headers=upload_headers,
                          data=open(image_file_path, "rb"))

            images_ids.append(register_post['value']['asset'])
        except Exception as e:
            print(e)

Post Them

The same details as when you're posting plain text, same endpoint same headers you'll just need to list the uploaded images to your new request body, like this:

{
    "author": "<USER_URN>",
    "lifecycleState": "PUBLISHED",
    "specificContent": {
        "com.linkedin.ugc.ShareContent": {
            "shareCommentary": {
                "id": "urn:li:share:your_share_id",
                "text": "<YOUR_POST_TEXT>"
            },
            "shareMediaCategory": "IMAGE",
            "media": [{
                            "status": "READY",
                            "media": "<ASSET_URN>"
            }]
        }
    },
    "visibility": {
        "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
    }
}

<ASSET_URN> is what you got from the registration request.

Here is Python code of how you might do it:

import requests
access_token = "user-access-token"
user_urn = "user-urn"
content = "LinkedIn API post test"
images_ids = ['asset', 'urn', 'for', 'the', 'images', 'uploaded']

api_url = 'https://api.linkedin.com/v2/ugcPosts'

headers = {
    'Authorization': f'Bearer {access_token}',
    'Connection': 'Keep-Alive',
    'Linkedin-Version': "format AAAAMM",
    "X-Restli-Protocol-Version": '2.0.0',
    'Content-Type': 'application/json',
}

post_body = {
    "author": user_urn,
    "lifecycleState": "PUBLISHED",
    "specificContent": {
        "com.linkedin.ugc.ShareContent": {
            "shareCommentary": {
                "text": content
            },
            "shareMediaCategory": "IMAGE",
            "media": [{
                            "status": "READY",
                            "media": img_id,
                        } for img_id in images_ids]
        }
    },
    "visibility":  {
        "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
    }
}

response = requests.post(api_url, headers=headers, json=post_body)
if response.status_code == 201:
    print("posted successfully", response.text)
else:
    print(f'{response.status_code}: {response.text}')

You can check how I implemented this into my recent project Time Logger in linekedin.py here.

Buy Me a Coffee

0
Subscribe to my newsletter

Read articles from Esam Hagalbashir directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Esam Hagalbashir
Esam Hagalbashir

I have only been working on personal projects for my own convenience. I'm looking to get my first job in the industry, and I'll share what I can from the projects I built that I'm currently using or collecting dust.