1

I'm trying to automate LinkedIn post publishing for a personal profile using Python and the LinkedIn UGC API. I'm authenticating via OAuth 2.0 (authorization code grant), and using a valid access token with the w_member_social scope.

The media (an image in .png format) is successfully uploaded to LinkedIn using the assets?action=registerUpload and subsequent PUT request. LinkedIn returns a valid asset URN, and the image upload succeeds with HTTP 201.

However, when I try to publish the post using the /v2/ugcPosts endpoint, I consistently receive a 500 Internal Server Error from LinkedIn. Here's a simplified version of the relevant parts of my script:

**UplUpload image **

register_data = { "registerUploadRequest": { "owner": "urn:li:me", "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], "serviceRelationships": [{ "identifier": "urn:li:userGeneratedContent", "relationshipType": "OWNER" }], "supportedUploadMechanism": ["SYNCHRONOUS_UPLOAD"] } } 

The upload succeeds, and I receive the asset URN (e.g., urn:li:digitalmediaAsset:C4E22AQF...).

UGC post payload:

data = { "lifecycleState": "PUBLISHED", "specificContent": { "com.linkedin.ugc.ShareContent": { "shareCommentary": { "text": "Test post from script" }, "shareMediaCategory": "IMAGE", "media": [{ "status": "READY", "media": "urn:li:digitalmediaAsset:C4E22AQF..." }] } }, "visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" } } 

The response from the POST request to https://api.linkedin.com/v2/ugcPosts is always:

{"message": "Internal Server Error", "status": 500} 

Things I've tried: Changing the mediaCategory to NONE (post without image) → still fails.

Removing the media block altogether → same error.

Printing and validating the full JSON before sending → no issues found.

Posting with cURL instead of requests → same result.

Ensured the upload is via urn:li:me (since I'm using a personal access token).

My access token is valid and includes the w_member_social scope.

What could cause LinkedIn's UGC API to return a 500 error when all parameters seem valid and the image upload succeeded? Is there an undocumented requirement or additional field I may be missing?

Code Complete

import os import json import time import requests import mysql.connector from datetime import datetime # Configuration CLIENT_ID = 'xxxxxxxxxxxxxxxx' CLIENT_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' REDIRECT_URI = 'http://localhost:8000' TOKEN_FILE = 'token.json' # This was originally for organization posting (not used for personal profile posting) LINKEDIN_ORGANIZATION_ID = 'urn:li:organization:xxxxxxxx' DB_CONFIG = { 'host': 'localhost', 'user': 'root', 'password': '*****', 'database': 'kerket' } # Save the token to a file def save_token(data): with open(TOKEN_FILE, 'w') as f: json.dump(data, f) # Load the token from a file def load_token(): if not os.path.exists(TOKEN_FILE): return None with open(TOKEN_FILE, 'r') as f: return json.load(f) # Validate expiration def is_token_expired(token_data): return datetime.utcnow().timestamp() > token_data.get('expires_at', 0) # Get valid token (currently expiration is not enforced) def get_valid_token(): token_data = load_token() return token_data['access_token'] # Upload an image or video file to LinkedIn and return asset URN and category def upload_media_to_linkedin(filepath): filename = os.path.basename(filepath) ext = filename.lower().split('.')[-1] if ext in ['jpg', 'jpeg', 'png']: recipe = "urn:li:digitalmediaRecipe:feedshare-image" category = "IMAGE" content_type = "image/jpeg" if ext in ['jpg', 'jpeg'] else "image/png" elif ext == 'mp4': recipe = "urn:li:digitalmediaRecipe:feedshare-video" category = "VIDEO" content_type = "application/octet-stream" else: print(f"[!] Unsupported format: {filepath}") return None, None token = get_valid_token() if not token: return None, None headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json" } print(f"[INFO] Uploading {filename} to LinkedIn...") print(f"[INFO] Content-Type: {content_type}") print(f"[INFO] Recipe: {recipe}") # Register the upload register_data = { "registerUploadRequest": { "owner": "urn:li:me", # Works with personal profile token "recipes": [recipe], "serviceRelationships": [{ "identifier": "urn:li:userGeneratedContent", "relationshipType": "OWNER" }], "supportedUploadMechanism": ["SYNCHRONOUS_UPLOAD"] } } r = requests.post("https://api.linkedin.com/v2/assets?action=registerUpload", headers=headers, json=register_data) if r.status_code != 200: print("[X] Media registration failed:", r.text) return None, None upload_info = r.json() upload_url = upload_info['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'] asset_urn = upload_info['value']['asset'] print(f"[INFO] Upload URL: {upload_url}") # Upload the file to LinkedIn with open(filepath, 'rb') as f: upload_headers = { "Authorization": f"Bearer {token}", "Content-Type": content_type } r2 = requests.put(upload_url, headers=upload_headers, data=f) if r2.status_code in [200, 201]: print(f"[✓] Uploaded: {filename}") return asset_urn, category else: print(f"[X] Upload failed for {filename}:", r2.text) return None, None # Publish a post on LinkedIn using a personal token def publish_to_linkedin(title, description, media_urns, category): token = get_valid_token() if not token: return False headers = { "Authorization": f"Bearer {token}", "X-Restli-Protocol-Version": "2.0.0", "Content-Type": "application/json" } data = { "lifecycleState": "PUBLISHED", "specificContent": { "com.linkedin.ugc.ShareContent": { "shareCommentary": { "text": f"{title}\n\n{description}" }, "shareMediaCategory": category if media_urns else "NONE", "media": [{"status": "READY", "media": urn} for urn in media_urns] if media_urns else [] } }, "visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" } } r = requests.post("https://api.linkedin.com/v2/ugcPosts", headers=headers, json=data) print("[DEBUG] LinkedIn response:", r.text) if r.status_code == 201: return True else: print("[X] Error posting:", r.text) return False # Main job: publish one pending post from the DB def check_and_publish(): conn = mysql.connector.connect(**DB_CONFIG) cursor = conn.cursor(dictionary=True) cursor.execute(""" SELECT * FROM publicaciones WHERE status = 'pendiente' AND linkedin IS NULL ORDER BY id ASC LIMIT 1 """) pub = cursor.fetchone() if pub: cursor.execute("SELECT archivo FROM archivos WHERE id_publicacion = %s", (pub['id'],)) files = cursor.fetchall() media_urns = [] media_category = None for f in files: path = f['archivo'] if os.path.exists(path): urn, cat = upload_media_to_linkedin(path) if urn: media_urns.append(urn) media_category = cat if publish_to_linkedin(pub['titulo'], pub['descripcion'], media_urns, media_category): now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') cursor.execute("UPDATE publicaciones SET linkedin = %s, status = 'publicado' WHERE id = %s", (now, pub['id'])) conn.commit() print(f"[✓] Published post ID {pub['id']}") else: print("[X] Failed to publish.") cursor.close() conn.close() # Cron job entry point if __name__ == "__main__": print("[INFO] LinkedIn publishing job started...") check_and_publish() 
1
  • Have you managed to get it to work? We're facing the same issue - Linkedin API docs and error messages need a total re-write to match their actual signatures Commented Jun 20 at 12:49

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.