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, ®ister_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
- For 403, ensure the app has the Share on LinkedIn product and the token has w_member_social.
- Get person_id from get_profile() using the same token you pass to upload_image — the owner URN must match the token owner.
- For 401, refresh the access token first.
- 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
- Always pair upload_image's token and person_id from the same get_profile call.
- Confirm the app includes the Share on LinkedIn product before shipping image features.
- Validate image bytes and MIME type locally first — size/type errors otherwise surface only at upload.
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
- LinkedIn create_post failed ({}): {}
- LinkedIn list_posts failed ({}): {}
- LinkedIn add_comment failed ({}): {}
- LinkedIn add_reaction failed ({}): {}
- LinkedIn delete_post failed ({}): {}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3167a3fc50472ba4.
Report an issue: GitHub.