Skip to main content

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.