[Python] LinkedIn API returns 500 Internal Server Error when creating a UGC post with image...

Discussão em 'Python' iniciado por Stack, Maio 9, 2025.

  1. Stack

    Stack Membro Participativo

    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:eek:rganization: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()

    Continue reading...

Compartilhe esta Página