Overview
Completed runs can include screenshots in run_message_history. These screenshots are stored as stable supabase://run-images/... references, not public HTTPS URLs:
{
"type": "image",
"image_url": "supabase://run-images/message-history/org_123/1762547216743_5002.png"
}
Use the run screenshot signed URL endpoint when you need to programmatically view, download, archive, or audit those images outside the Cyberdesk dashboard.
Why Signed URLs?
Run screenshots are private organization data. The supabase:// reference is stable and safe to store, but it is not directly fetchable. A signed URL is a temporary HTTPS URL that grants read access to one screenshot.
Signed URLs expire after the requested expires_in value. The maximum is 3600 seconds.
Authorization Model
The endpoint is scoped to both the run and the image reference:
- Your API key must belong to the run’s organization.
- The
image_url must be an exact supabase://run-images/... URL present in that run’s run_message_history.
- Cyberdesk verifies the storage path belongs to the same organization before returning a signed URL.
This means you can safely pass the screenshot references you read from a run response without exposing other organizations’ images.
Endpoint
GET /v1/runs/{run_id}/image/signed-url?image_url={image_url}&expires_in=3600
Authorization: Bearer cd_...
Example response:
{
"supabase_url": "supabase://run-images/message-history/org_123/1762547216743_5002.png",
"signed_url": "https://...",
"expires_in": 3600
}
Curl Example
curl -G "https://api.cyberdesk.io/v1/runs/$RUN_ID/image/signed-url" \
-H "Authorization: Bearer $CYBERDESK_API_KEY" \
--data-urlencode "image_url=supabase://run-images/message-history/org_123/1762547216743_5002.png" \
--data-urlencode "expires_in=3600"
TypeScript Example
const { data: run, error } = await client.runs.get("run-id");
if (error || !run?.run_message_history) {
throw error ?? new Error("Run has no message history");
}
const imageUrl = run.run_message_history
.flatMap((message) => Array.isArray(message.content) ? message.content : [])
.find((block) => block?.type === "image" && typeof block.image_url === "string")
?.image_url;
if (!imageUrl) {
throw new Error("No screenshot found in run message history");
}
const { data: signed } = await client.runs.getImageSignedUrl("run-id", imageUrl, {
expires_in: 3600,
});
console.log(signed?.signed_url);
Python Example
response = await client.runs.get("run-id")
if response.error or not response.data or not response.data.run_message_history:
raise response.error or RuntimeError("Run has no message history")
image_url = None
for message in response.data.run_message_history:
content = message.get("content", [])
if not isinstance(content, list):
continue
for block in content:
if block.get("type") == "image" and isinstance(block.get("image_url"), str):
image_url = block["image_url"]
break
if image_url:
break
if not image_url:
raise RuntimeError("No screenshot found in run message history")
signed = await client.runs.get_image_signed_url(
"run-id",
image_url=image_url,
expires_in=3600,
)
print(signed.data.signed_url)
Common Errors
400 Invalid Supabase URL: The image_url is not a valid supabase://... reference.
400 Only run-images URLs are supported: The URL points to a different storage bucket.
403 Image URL is not present in this run's message history: The URL was not read from the specified run.
403 Image URL does not belong to the organization: The storage path does not match the authenticated organization.
404 Run not found: The run ID does not exist in the authenticated organization.