zeroclaw-labs/zeroclaw · error

LinkedIn image register failed ({status}): {body_text}

Error message

LinkedIn image register failed ({status}): {body_text}

What it means

Thrown in step 1 of upload_image when POST {LINKEDIN_API_BASE}/rest/images?action=initializeUpload returns non-2xx, body included. The registration body only carries the owner URN (urn:li:person:{person_id}), so failures are almost always scope/permission or owner mismatches rather than payload complexity. On success the response's uploadUrl is used for the binary PUT, so this guard fires before any bytes are uploaded.

Source

Thrown at crates/zeroclaw-tools/src/linkedin_client.rs:615

        person_id: &str,
    ) -> anyhow::Result<String> {
        let owner_urn = format!("urn:li:person:{person_id}");

        // Step 1: Register upload
        let register_body = json!({
            "initializeUploadRequest": {
                "owner": owner_urn
            }
        });
        let register_url = format!("{LINKEDIN_API_BASE}/rest/images?action=initializeUpload");
        let register_resp = self
            .api_request(Method::POST, &register_url, token, Some(register_body))
            .await?;

        let status = register_resp.status();
        if !status.is_success() {
            let body_text = register_resp.text().await.unwrap_or_default();
            anyhow::bail!("LinkedIn image register failed ({status}): {body_text}");
        }

        let register_json: serde_json::Value = register_resp
            .json()
            .await
            .context("Failed to parse image register response")?;

        let upload_url = register_json
            .pointer("/value/uploadUrl")
            .and_then(|v| v.as_str())
            .ok_or_else(|| {
                ::zeroclaw_log::record!(
                    ERROR,
                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                        .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                        .with_attrs(::serde_json::json!({"missing": "uploadUrl"})),
                    "linkedin_client: register response missing uploadUrl"
                );

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. For 403, ensure the app has the Share on LinkedIn product and the token has w_member_social.
  2. Get person_id from get_profile() using the same token you pass to upload_image — the owner URN must match the token owner.
  3. For 401, refresh the access token first.
  4. Verify the exact error in the response body; LinkedIn typically names the missing permission.

Example fix

// before: person_id from a hardcoded/config value of another member -> 403
client.upload_image(&bytes, &token, &config_person_id).await?;

// after: derive the owner from the token's own profile
let profile = client.get_profile().await?;
client.upload_image(&bytes, &token, &profile.id).await?;
Defensive patterns

Strategy: try-catch

Validate before calling

// Owner must match the token: derive person_id from the same credentials
let profile = client.get_profile().await?; // uses same stored creds
// pass profile.id as person_id to upload_image

Try / catch

match client.upload_image(&bytes, &token, &person_id).await {
    Ok(urn) => Ok(urn),
    Err(e) if e.to_string().contains("(403") => {
        Err(e).context("image upload denied; check w_member_social scope and owner URN")
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Token lacking w_member_social (403); person_id not matching the token owner so the owner URN is rejected (403/400); expired token (401); app not approved for image upload.

Common situations: Text posting works (token has scope) but images fail because the product/scope configuration differs; person id taken from a different profile than the authorized member; stale member ids cached after re-authorization.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/3167a3fc50472ba4. Report an issue: GitHub.