Skip to content

Latest commit

 

History

History
1281 lines (1063 loc) · 36 KB

File metadata and controls

1281 lines (1063 loc) · 36 KB
title Cloud API Reference
description Complete API reference with code examples for Comfy Cloud

import PollJobCompletion from '/snippets/cloud/poll-job-completion.mdx' import WebSocketProgress from '/snippets/cloud/websocket-progress.mdx' import DownloadOutputs from '/snippets/cloud/download-outputs.mdx'

**Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. Some endpoints are maintained for compatibility with local ComfyUI but may have different semantics (e.g., ignored fields).

This page provides complete examples for common Comfy Cloud API operations.

**Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api) for details.

Setup

All examples use these common imports and configuration:

```bash curl export COMFY_CLOUD_API_KEY="your-api-key" export BASE_URL="https://cloud.comfy.org" ```
import { readFile, writeFile } from "fs/promises";

const BASE_URL = "https://cloud.comfy.org";
const API_KEY = process.env.COMFY_CLOUD_API_KEY!;

function getHeaders(): HeadersInit {
  return {
    "X-API-Key": API_KEY,
    "Content-Type": "application/json",
  };
}
import os
import requests
import json
import time
import asyncio
import aiohttp

BASE_URL = "https://cloud.comfy.org"
API_KEY = os.environ["COMFY_CLOUD_API_KEY"]

def get_headers():
    return {
        "X-API-Key": API_KEY,
        "Content-Type": "application/json"
    }

Object Info

Retrieve available node definitions. This is useful for understanding what nodes are available and their input/output specifications.

```bash curl curl -X GET "$BASE_URL/api/object_info" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ```
async function getObjectInfo(): Promise<Record<string, any>> {
  const response = await fetch(`${BASE_URL}/api/object_info`, {
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const objectInfo = await getObjectInfo();
console.log(`Available nodes: ${Object.keys(objectInfo).length}`);

const ksampler = objectInfo["KSampler"] ?? {};
console.log(`KSampler inputs: ${Object.keys(ksampler.input?.required ?? {})}`);
def get_object_info():
    """Fetch all available node definitions from cloud."""
    response = requests.get(
        f"{BASE_URL}/api/object_info",
        headers=get_headers()
    )
    response.raise_for_status()
    return response.json()

# Get all nodes
object_info = get_object_info()
print(f"Available nodes: {len(object_info)}")

# Get a specific node's definition
ksampler = object_info.get("KSampler", {})
inputs = list(ksampler.get('input', {}).get('required', {}).keys())
print(f"KSampler inputs: {inputs}")

Assets API

The Assets API provides a unified, tag-based system for managing files (images, models, outputs) with content-addressed storage and rich metadata support.

Key Features

  • Content-addressed storage: Files are deduplicated using Blake3 hashes
  • Tag-based organization: Flexible tagging instead of folder hierarchies
  • Metadata support: Attach custom JSON metadata to assets
  • Filtering and search: Query assets by tags, name, or metadata

List Assets

Retrieve a paginated list of your assets with optional filtering.

```bash curl # List all assets curl -X GET "$BASE_URL/api/assets" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY"

Filter by tags

curl -X GET "$BASE_URL/api/assets?include_tags=output,image"
-H "X-API-Key: $COMFY_CLOUD_API_KEY"

Search by name

curl -X GET "$BASE_URL/api/assets?name_contains=portrait"
-H "X-API-Key: $COMFY_CLOUD_API_KEY"


```typescript TypeScript
interface Asset {
  id: string;
  name: string;
  hash: string;
  size_bytes: number;
  mime_type: string;
  tags: string[];
  created_at: string;
  preview_url?: string;
}

interface ListAssetsResponse {
  assets: Asset[];
  total: number;
  offset: number;
  limit: number;
}

async function listAssets(options: {
  include_tags?: string[];
  exclude_tags?: string[];
  name_contains?: string;
  sort?: "name" | "created_at" | "size";
  order?: "asc" | "desc";
  offset?: number;
  limit?: number;
} = {}): Promise<ListAssetsResponse> {
  const params = new URLSearchParams();
  if (options.include_tags?.length) {
    params.set("include_tags", options.include_tags.join(","));
  }
  if (options.exclude_tags?.length) {
    params.set("exclude_tags", options.exclude_tags.join(","));
  }
  if (options.name_contains) params.set("name_contains", options.name_contains);
  if (options.sort) params.set("sort", options.sort);
  if (options.order) params.set("order", options.order);
  if (options.offset !== undefined) params.set("offset", String(options.offset));
  if (options.limit !== undefined) params.set("limit", String(options.limit));

  const response = await fetch(`${BASE_URL}/api/assets?${params}`, {
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

// List output images
const { assets } = await listAssets({ include_tags: ["output", "image"] });
console.log(`Found ${assets.length} output images`);
def list_assets(
    include_tags: list = None,
    exclude_tags: list = None,
    name_contains: str = None,
    sort: str = "created_at",
    order: str = "desc",
    offset: int = 0,
    limit: int = 20
) -> dict:
    """List assets with optional filtering.
    
    Args:
        include_tags: Only include assets with ALL these tags
        exclude_tags: Exclude assets with ANY of these tags
        name_contains: Filter by name substring (case-insensitive)
        sort: Sort field (name, created_at, size, updated_at)
        order: Sort direction (asc or desc)
        offset: Pagination offset
        limit: Max items per page (1-500)
        
    Returns:
        Dict with 'assets' array and pagination info
    """
    params = {
        "sort": sort,
        "order": order,
        "offset": offset,
        "limit": limit
    }
    if include_tags:
        params["include_tags"] = ",".join(include_tags)
    if exclude_tags:
        params["exclude_tags"] = ",".join(exclude_tags)
    if name_contains:
        params["name_contains"] = name_contains
    
    response = requests.get(
        f"{BASE_URL}/api/assets",
        headers=get_headers(),
        params=params
    )
    response.raise_for_status()
    return response.json()

# List all output images
result = list_assets(include_tags=["output"])
print(f"Found {len(result['assets'])} output assets")

Upload Asset

Upload a file with tags and metadata.

```bash curl curl -X POST "$BASE_URL/api/assets" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -F "file=@image.png" \ -F "tags=input,reference" \ -F "name=My Reference Image" ```
async function uploadAsset(
  file: File | Blob,
  options: {
    name?: string;
    tags?: string[];
    mime_type?: string;
    user_metadata?: Record<string, any>;
  } = {}
): Promise<Asset> {
  const formData = new FormData();
  formData.append("file", file);
  if (options.name) formData.append("name", options.name);
  if (options.tags?.length) formData.append("tags", options.tags.join(","));
  if (options.mime_type) formData.append("mime_type", options.mime_type);
  if (options.user_metadata) {
    formData.append("user_metadata", JSON.stringify(options.user_metadata));
  }

  const response = await fetch(`${BASE_URL}/api/assets`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY },
    body: formData,
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

// Upload with tags
const asset = await uploadAsset(imageBlob, {
  name: "reference.png",
  tags: ["input", "reference"],
});
console.log(`Uploaded: ${asset.id}`);
def upload_asset(
    file_path: str,
    name: str = None,
    tags: list = None,
    user_metadata: dict = None
) -> dict:
    """Upload an asset with optional metadata.
    
    Args:
        file_path: Path to the file to upload
        name: Display name for the asset
        tags: List of tags to apply
        user_metadata: Custom JSON metadata
        
    Returns:
        Created asset details
    """
    with open(file_path, "rb") as f:
        files = {"file": f}
        data = {}
        if name:
            data["name"] = name
        if tags:
            data["tags"] = ",".join(tags)
        if user_metadata:
            data["user_metadata"] = json.dumps(user_metadata)
        
        response = requests.post(
            f"{BASE_URL}/api/assets",
            headers={"X-API-Key": API_KEY},
            files=files,
            data=data
        )
    response.raise_for_status()
    return response.json()

# Upload with tags
asset = upload_asset("image.png", name="reference", tags=["input", "reference"])
print(f"Uploaded: {asset['id']}")

Get Asset Details

Retrieve full details for a specific asset.

```bash curl curl -X GET "$BASE_URL/api/assets/{asset_id}" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ```
async function getAsset(assetId: string): Promise<Asset> {
  const response = await fetch(`${BASE_URL}/api/assets/${assetId}`, {
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
def get_asset(asset_id: str) -> dict:
    """Get asset details by ID."""
    response = requests.get(
        f"{BASE_URL}/api/assets/{asset_id}",
        headers=get_headers()
    )
    response.raise_for_status()
    return response.json()

Manage Tags

Add or remove tags from assets.

```bash curl # Add tags curl -X POST "$BASE_URL/api/assets/{asset_id}/tags" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"tags": ["favorite", "portrait"]}'

Remove tags

curl -X DELETE "$BASE_URL/api/assets/{asset_id}/tags"
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
-H "Content-Type: application/json"
-d '{"tags": ["temporary"]}'


```typescript TypeScript
async function addAssetTags(assetId: string, tags: string[]): Promise<void> {
  const response = await fetch(`${BASE_URL}/api/assets/${assetId}/tags`, {
    method: "POST",
    headers: getHeaders(),
    body: JSON.stringify({ tags }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
}

async function removeAssetTags(assetId: string, tags: string[]): Promise<void> {
  const response = await fetch(`${BASE_URL}/api/assets/${assetId}/tags`, {
    method: "DELETE",
    headers: getHeaders(),
    body: JSON.stringify({ tags }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
}
def add_asset_tags(asset_id: str, tags: list) -> dict:
    """Add tags to an asset."""
    response = requests.post(
        f"{BASE_URL}/api/assets/{asset_id}/tags",
        headers=get_headers(),
        json={"tags": tags}
    )
    response.raise_for_status()
    return response.json()

def remove_asset_tags(asset_id: str, tags: list) -> dict:
    """Remove tags from an asset."""
    response = requests.delete(
        f"{BASE_URL}/api/assets/{asset_id}/tags",
        headers=get_headers(),
        json={"tags": tags}
    )
    response.raise_for_status()
    return response.json()

Delete Asset

```bash curl curl -X DELETE "$BASE_URL/api/assets/{asset_id}" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ```
async function deleteAsset(assetId: string): Promise<void> {
  const response = await fetch(`${BASE_URL}/api/assets/${assetId}`, {
    method: "DELETE",
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
}
def delete_asset(asset_id: str) -> None:
    """Delete an asset."""
    response = requests.delete(
        f"{BASE_URL}/api/assets/{asset_id}",
        headers=get_headers()
    )
    response.raise_for_status()

Uploading Inputs (Legacy)

**Legacy Endpoints:** These endpoints (`/api/upload/*`) are maintained for compatibility with local ComfyUI. For new integrations, consider using the [Assets API](#assets-api) when available.

Upload images, masks, or other files for use in workflows.

Direct Upload (Multipart)

```bash curl curl -X POST "$BASE_URL/api/upload/image" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -F "image=@my_image.png" \ -F "type=input" \ -F "overwrite=true" ```
async function uploadInput(
  filePath: string,
  inputType: string = "input"
): Promise<{ name: string; subfolder: string }> {
  const file = await readFile(filePath);
  const formData = new FormData();
  formData.append("image", new Blob([file]), filePath.split("/").pop());
  formData.append("type", inputType);
  formData.append("overwrite", "true");

  const response = await fetch(`${BASE_URL}/api/upload/image`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY },
    body: formData,
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const result = await uploadInput("my_image.png");
console.log(`Uploaded: ${result.name} to ${result.subfolder}`);
def upload_input(file_path: str, input_type: str = "input") -> dict:
    """Upload a file directly to cloud.
    
    Args:
        file_path: Path to the file to upload
        input_type: "input" for images, "temp" for temporary files
        
    Returns:
        Upload response with filename and subfolder
    """
    with open(file_path, "rb") as f:
        files = {"image": f}
        data = {"type": input_type, "overwrite": "true"}
        
        response = requests.post(
            f"{BASE_URL}/api/upload/image",
            headers={"X-API-Key": API_KEY},  # No Content-Type for multipart
            files=files,
            data=data
        )
    response.raise_for_status()
    return response.json()

# Upload an image
result = upload_input("my_image.png")
print(f"Uploaded: {result['name']} to {result['subfolder']}")

Upload Mask

The `subfolder` parameter is accepted for API compatibility but ignored in cloud storage. All files are stored in a flat, content-addressed namespace. ```bash curl curl -X POST "$BASE_URL/api/upload/mask" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -F "image=@mask.png" \ -F "type=input" \ -F "subfolder=clipspace" \ -F 'original_ref={"filename":"my_image.png","subfolder":"","type":"input"}' ```
async function uploadMask(
  filePath: string,
  originalRef: { filename: string; subfolder: string; type: string }
): Promise<{ name: string; subfolder: string }> {
  const file = await readFile(filePath);
  const formData = new FormData();
  formData.append("image", new Blob([file]), filePath.split("/").pop());
  formData.append("type", "input");
  formData.append("subfolder", "clipspace");
  formData.append("original_ref", JSON.stringify(originalRef));

  const response = await fetch(`${BASE_URL}/api/upload/mask`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY },
    body: formData,
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const maskResult = await uploadMask("mask.png", {
  filename: "my_image.png",
  subfolder: "",
  type: "input",
});
console.log(`Uploaded mask: ${maskResult.name}`);
def upload_mask(file_path: str, original_ref: dict) -> dict:
    """Upload a mask associated with an original image.
    
    Args:
        file_path: Path to the mask file
        original_ref: Reference to the original image {"filename": "...", "subfolder": "...", "type": "..."}
    """
    with open(file_path, "rb") as f:
        files = {"image": f}
        data = {
            "type": "input",
            "subfolder": "clipspace",
            "original_ref": json.dumps(original_ref)
        }
        
        response = requests.post(
            f"{BASE_URL}/api/upload/mask",
            headers={"X-API-Key": API_KEY},
            files=files,
            data=data
        )
    response.raise_for_status()
    return response.json()

Running Workflows

Submit a workflow for execution.

Submit Workflow

```bash curl curl -X POST "$BASE_URL/api/prompt" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": '"$(cat workflow_api.json)"'}' ```
async function submitWorkflow(workflow: Record<string, any>): Promise<string> {
  const response = await fetch(`${BASE_URL}/api/prompt`, {
    method: "POST",
    headers: getHeaders(),
    body: JSON.stringify({ prompt: workflow }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const result = await response.json();

  if (result.error) {
    throw new Error(`Workflow error: ${result.error}`);
  }
  return result.prompt_id;
}

const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8"));
const promptId = await submitWorkflow(workflow);
console.log(`Submitted job: ${promptId}`);
def submit_workflow(workflow: dict) -> str:
    """Submit a workflow and return the prompt_id (job ID).
    
    Args:
        workflow: ComfyUI workflow in API format
        
    Returns:
        prompt_id for tracking the job
    """
    response = requests.post(
        f"{BASE_URL}/api/prompt",
        headers=get_headers(),
        json={"prompt": workflow}
    )
    response.raise_for_status()
    result = response.json()
    
    if "error" in result:
        raise ValueError(f"Workflow error: {result['error']}")
    
    return result["prompt_id"]

# Load and submit a workflow
with open("workflow_api.json") as f:
    workflow = json.load(f)

prompt_id = submit_workflow(workflow)
print(f"Submitted job: {prompt_id}")

Using Partner Nodes

If your workflow contains Partner Nodes (nodes that call external AI services like Flux Pro, Ideogram, etc.), you must include your Comfy API key in the extra_data field of the request payload.

The ComfyUI frontend automatically packages your API key into `extra_data` when running workflows in the browser. This section is only relevant when calling the API directly. ```bash curl curl -X POST "$BASE_URL/api/prompt" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "prompt": '"$(cat workflow_api.json)"', "extra_data": { "api_key_comfy_org": "your-comfy-api-key" } }' ```
async function submitWorkflowWithPartnerNodes(
  workflow: Record<string, any>,
  apiKey: string
): Promise<string> {
  const response = await fetch(`${BASE_URL}/api/prompt`, {
    method: "POST",
    headers: getHeaders(),
    body: JSON.stringify({
      prompt: workflow,
      extra_data: {
        api_key_comfy_org: apiKey,
      },
    }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  const result = await response.json();
  return result.prompt_id;
}

// Use when workflow contains Partner Nodes (e.g., Flux Pro, Ideogram, etc.)
const promptId = await submitWorkflowWithPartnerNodes(workflow, API_KEY);
def submit_workflow_with_partner_nodes(workflow: dict, api_key: str) -> str:
    """Submit a workflow that uses Partner Nodes.
    
    Args:
        workflow: ComfyUI workflow in API format
        api_key: Your API key from platform.comfy.org
        
    Returns:
        prompt_id for tracking the job
    """
    response = requests.post(
        f"{BASE_URL}/api/prompt",
        headers=get_headers(),
        json={
            "prompt": workflow,
            "extra_data": {
                "api_key_comfy_org": api_key
            }
        }
    )
    response.raise_for_status()
    return response.json()["prompt_id"]

# Use when workflow contains Partner Nodes
prompt_id = submit_workflow_with_partner_nodes(workflow, API_KEY)
Generate your API key at [platform.comfy.org](https://platform.comfy.org/login). This is the same key used for Cloud API authentication (`X-API-Key` header).

Modify Workflow Inputs

```typescript TypeScript function setWorkflowInput( workflow: Record, nodeId: string, inputName: string, value: any ): Record { if (workflow[nodeId]) { workflow[nodeId].inputs[inputName] = value; } return workflow; }

// Example: Set seed and prompt let workflow = JSON.parse(await readFile("workflow_api.json", "utf-8")); workflow = setWorkflowInput(workflow, "3", "seed", 12345); workflow = setWorkflowInput(workflow, "6", "text", "a beautiful landscape");


```python Python
def set_workflow_input(workflow: dict, node_id: str, input_name: str, value) -> dict:
    """Modify a workflow input value.
    
    Args:
        workflow: The workflow dict
        node_id: ID of the node to modify
        input_name: Name of the input field
        value: New value
        
    Returns:
        Modified workflow
    """
    if node_id in workflow:
        workflow[node_id]["inputs"][input_name] = value
    return workflow

# Example: Set seed and prompt
workflow = set_workflow_input(workflow, "3", "seed", 12345)
workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape")

Checking Job Status

Poll for job completion.


WebSocket for Real-Time Progress

Connect to the WebSocket for real-time execution updates.

The `clientId` parameter is currently ignored—all connections for a user receive the same messages. Pass a unique `clientId` for forward compatibility.

WebSocket Message Types

Messages are sent as JSON text frames unless otherwise noted.

Type Description
status Queue status update with queue_remaining count
notification User-friendly status message (value field contains text like "Executing workflow...")
execution_start Workflow execution has started
executing A specific node is now executing (node ID in node field)
progress Step progress within a node (value/max for sampling steps)
progress_state Extended progress state with node metadata (nested nodes object)
executed Node completed with outputs (images, video, etc. in output field)
execution_cached Nodes skipped because outputs are cached (nodes array)
execution_success Entire workflow completed successfully
execution_error Workflow failed (includes exception_type, exception_message, traceback)
execution_interrupted Workflow was cancelled by user

Binary Messages (Preview Images)

During image generation, ComfyUI sends binary WebSocket frames containing preview images. These are raw binary data (not JSON):

Binary Type Value Description
PREVIEW_IMAGE 1 In-progress preview during diffusion sampling
TEXT 3 Text output from nodes (progress text)
PREVIEW_IMAGE_WITH_METADATA 4 Preview image with node context metadata

Binary frame formats (all integers are big-endian):

| Offset | Size | Field | Description | |--------|------|-------|-------------| | 0 | 4 bytes | `type` | `0x00000001` | | 4 | 4 bytes | `image_type` | Format code (1=JPEG, 2=PNG) | | 8 | variable | `image_data` | Raw image bytes | | Offset | Size | Field | Description | |--------|------|-------|-------------| | 0 | 4 bytes | `type` | `0x00000003` | | 4 | 4 bytes | `node_id_len` | Length of node_id string | | 8 | N bytes | `node_id` | UTF-8 encoded node ID | | 8+N | variable | `text` | UTF-8 encoded progress text | | Offset | Size | Field | Description | |--------|------|-------|-------------| | 0 | 4 bytes | `type` | `0x00000004` | | 4 | 4 bytes | `metadata_len` | Length of metadata JSON | | 8 | N bytes | `metadata` | UTF-8 JSON (see below) | | 8+N | variable | `image_data` | Raw JPEG/PNG bytes |
**Metadata JSON structure:**
```json
{
  "node_id": "3",
  "display_node_id": "3",
  "real_node_id": "3",
  "prompt_id": "abc-123",
  "parent_node_id": null
}
```
See the [OpenAPI Specification](/development/cloud/openapi) for complete schema definitions of each JSON message type.

Downloading Outputs

Retrieve generated files after job completion.


Complete End-to-End Example

Here's a full example that ties everything together:

```typescript TypeScript const BASE_URL = "https://cloud.comfy.org"; const API_KEY = process.env.COMFY_CLOUD_API_KEY!;

function getHeaders(): HeadersInit { return { "X-API-Key": API_KEY, "Content-Type": "application/json" }; }

async function submitWorkflow(workflow: Record<string, any>): Promise { const response = await fetch(${BASE_URL}/api/prompt, { method: "POST", headers: getHeaders(), body: JSON.stringify({ prompt: workflow }), }); if (!response.ok) throw new Error(HTTP ${response.status}); return (await response.json()).prompt_id; }

async function waitForCompletion( promptId: string, timeout: number = 300000 ): Promise<Record<string, any>> { const wsUrl = wss://cloud.comfy.org/ws?clientId=${crypto.randomUUID()}&token=${API_KEY}; const outputs: Record<string, any> = {};

return new Promise((resolve, reject) => { const ws = new WebSocket(wsUrl); const timer = setTimeout(() => { ws.close(); reject(new Error("Job timed out")); }, timeout);

ws.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.data?.prompt_id !== promptId) return;

  const msgType = data.type;
  const msgData = data.data ?? {};

  if (msgType === "progress") {
    console.log(`Progress: ${msgData.value}/${msgData.max}`);
  } else if (msgType === "executed" && msgData.output) {
    outputs[msgData.node] = msgData.output;
  } else if (msgType === "execution_success") {
    clearTimeout(timer);
    ws.close();
    resolve(outputs);
  } else if (msgType === "execution_error") {
    clearTimeout(timer);
    ws.close();
    reject(new Error(msgData.exception_message ?? "Unknown error"));
  }
};

ws.onerror = (err) => {
  clearTimeout(timer);
  reject(err);
};

}); }

async function downloadOutputs( outputs: Record<string, any>, outputDir: string ): Promise { for (const nodeOutputs of Object.values(outputs)) { for (const key of ["images", "video", "audio"]) { for (const fileInfo of (nodeOutputs as any)[key] ?? []) { const params = new URLSearchParams({ filename: fileInfo.filename, subfolder: fileInfo.subfolder ?? "", type: fileInfo.type ?? "output", }); // Get redirect URL (don't follow to avoid sending auth to storage) const response = await fetch(${BASE_URL}/api/view?${params}, { headers: { "X-API-Key": API_KEY }, redirect: "manual", }); if (response.status !== 302) throw new Error(HTTP ${response.status}); const signedUrl = response.headers.get("location")!; // Fetch from signed URL without auth headers const fileResponse = await fetch(signedUrl); if (!fileResponse.ok) throw new Error(HTTP ${fileResponse.status});

    const path = `${outputDir}/${fileInfo.filename}`;
    await writeFile(path, Buffer.from(await fileResponse.arrayBuffer()));
    console.log(`Downloaded: ${path}`);
  }
}

} }

async function main() { // 1. Load workflow const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8"));

// 2. Modify workflow parameters workflow["3"].inputs.seed = 42; workflow["6"].inputs.text = "a beautiful sunset over mountains";

// 3. Submit workflow const promptId = await submitWorkflow(workflow); console.log(Job submitted: ${promptId});

// 4. Wait for completion with progress const outputs = await waitForCompletion(promptId); console.log(Job completed! Found ${Object.keys(outputs).length} output nodes);

// 5. Download outputs await downloadOutputs(outputs, "./outputs"); console.log("Done!"); }

main();


```python Python
import os
import requests
import json
import asyncio
import aiohttp
import uuid

BASE_URL = "https://cloud.comfy.org"
API_KEY = os.environ["COMFY_CLOUD_API_KEY"]

def get_headers():
    return {"X-API-Key": API_KEY, "Content-Type": "application/json"}

def upload_image(file_path: str) -> dict:
    """Upload an image and return the reference for use in workflows."""
    with open(file_path, "rb") as f:
        response = requests.post(
            f"{BASE_URL}/api/upload/image",
            headers={"X-API-Key": API_KEY},
            files={"image": f},
            data={"type": "input", "overwrite": "true"}
        )
    response.raise_for_status()
    return response.json()

def submit_workflow(workflow: dict) -> str:
    """Submit workflow and return prompt_id."""
    response = requests.post(
        f"{BASE_URL}/api/prompt",
        headers=get_headers(),
        json={"prompt": workflow}
    )
    response.raise_for_status()
    return response.json()["prompt_id"]

async def wait_for_completion(prompt_id: str, timeout: float = 300.0) -> dict:
    """Wait for job completion via WebSocket."""
    ws_url = BASE_URL.replace("https://", "wss://") + f"/ws?clientId={uuid.uuid4()}&token={API_KEY}"
    outputs = {}
    
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(ws_url) as ws:
            start = asyncio.get_event_loop().time()
            async for msg in ws:
                if asyncio.get_event_loop().time() - start > timeout:
                    raise TimeoutError("Job timed out")
                
                if msg.type != aiohttp.WSMsgType.TEXT:
                    continue
                    
                data = json.loads(msg.data)
                if data.get("data", {}).get("prompt_id") != prompt_id:
                    continue
                
                msg_type = data.get("type")
                msg_data = data.get("data", {})
                
                if msg_type == "progress":
                    print(f"Progress: {msg_data.get('value')}/{msg_data.get('max')}")
                elif msg_type == "executed":
                    if output := msg_data.get("output"):
                        outputs[msg_data["node"]] = output
                elif msg_type == "execution_success":
                    return outputs
                elif msg_type == "execution_error":
                    raise RuntimeError(msg_data.get("exception_message", "Unknown error"))
    
    return outputs

def download_outputs(outputs: dict, output_dir: str):
    """Download all output files."""
    os.makedirs(output_dir, exist_ok=True)
    
    for node_outputs in outputs.values():
        for key in ["images", "video", "audio"]:
            for file_info in node_outputs.get(key, []):
                params = {
                    "filename": file_info["filename"],
                    "subfolder": file_info.get("subfolder", ""),
                    "type": file_info.get("type", "output")
                }
                response = requests.get(f"{BASE_URL}/api/view", headers=get_headers(), params=params)
                response.raise_for_status()
                
                path = os.path.join(output_dir, file_info["filename"])
                with open(path, "wb") as f:
                    f.write(response.content)
                print(f"Downloaded: {path}")

async def main():
    # 1. Load workflow
    with open("workflow_api.json") as f:
        workflow = json.load(f)
    
    # 2. Optionally upload input images
    # image_ref = upload_image("input.png")
    # workflow["1"]["inputs"]["image"] = image_ref["name"]
    
    # 3. Modify workflow parameters
    workflow["3"]["inputs"]["seed"] = 42
    workflow["6"]["inputs"]["text"] = "a beautiful sunset over mountains"
    
    # 4. Submit workflow
    prompt_id = submit_workflow(workflow)
    print(f"Job submitted: {prompt_id}")
    
    # 5. Wait for completion with progress
    outputs = await wait_for_completion(prompt_id)
    print(f"Job completed! Found {len(outputs)} output nodes")
    
    # 6. Download outputs
    download_outputs(outputs, "./outputs")
    print("Done!")

if __name__ == "__main__":
    asyncio.run(main())

Queue Management

Get Queue Status

```bash curl curl -X GET "$BASE_URL/api/queue" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ```
async function getQueue(): Promise<{
  queue_running: any[];
  queue_pending: any[];
}> {
  const response = await fetch(`${BASE_URL}/api/queue`, {
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

const queue = await getQueue();
console.log(`Running: ${queue.queue_running.length}`);
console.log(`Pending: ${queue.queue_pending.length}`);
def get_queue():
    """Get current queue status."""
    response = requests.get(
        f"{BASE_URL}/api/queue",
        headers=get_headers()
    )
    response.raise_for_status()
    return response.json()

queue = get_queue()
print(f"Running: {len(queue.get('queue_running', []))}")
print(f"Pending: {len(queue.get('queue_pending', []))}")

Cancel a Job

```bash curl curl -X POST "$BASE_URL/api/queue" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{"delete": ["PROMPT_ID_HERE"]}' ```
async function cancelJob(promptId: string): Promise<void> {
  const response = await fetch(`${BASE_URL}/api/queue`, {
    method: "POST",
    headers: getHeaders(),
    body: JSON.stringify({ delete: [promptId] }),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
}
def cancel_job(prompt_id: str):
    """Cancel a pending or running job."""
    response = requests.post(
        f"{BASE_URL}/api/queue",
        headers=get_headers(),
        json={"delete": [prompt_id]}
    )
    response.raise_for_status()
    return response.json()

Interrupt Current Execution

```bash curl curl -X POST "$BASE_URL/api/interrupt" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ```
async function interrupt(): Promise<void> {
  const response = await fetch(`${BASE_URL}/api/interrupt`, {
    method: "POST",
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
}
def interrupt():
    """Interrupt the currently running job."""
    response = requests.post(
        f"{BASE_URL}/api/interrupt",
        headers=get_headers()
    )
    response.raise_for_status()

Error Handling

HTTP Errors

REST API endpoints return standard HTTP status codes:

Status Description
400 Invalid request (bad workflow, missing fields)
401 Unauthorized (invalid or missing API key)
402 Insufficient credits
429 Subscription inactive
500 Internal server error

Execution Errors

During workflow execution, errors are delivered via the execution_error WebSocket message. The exception_type field identifies the error category:

Exception Type Description
ValidationError Invalid workflow or inputs
ModelDownloadError Required model not available or failed to download
ImageDownloadError Failed to download input image from URL
OOMError Out of GPU memory
InsufficientFundsError Account balance too low (for Partner Nodes)
InactiveSubscriptionError Subscription not active