Skip to main content
Experimental API: This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice.

Comfy Cloud API

The Comfy Cloud API provides programmatic access to run workflows on Comfy Cloud infrastructure. The API is compatible with local ComfyUI’s API, making it easy to migrate existing integrations.
Subscription required: API access is available on the Standard, Creator and Pro tiers. The Free tier does not include API access. See pricing plans for details.

Credits and Usage

API requests draw from the same monthly credit allocation as the Comfy Cloud web UI — there is no separate API credit pool. Each tier’s included credits, top-up options, and per-workflow runtime caps apply to API jobs in exactly the same way as UI jobs. See the pricing page for the monthly credit amounts on the Standard, Creator and Pro tiers. If you run out of credits mid-month, top-ups can be purchased from your account dashboard.

Base URL

https://cloud.comfy.org

Authentication

All API requests require an API key passed via the X-API-Key header.

Getting an API Key

See Getting an API Key for instructions on creating and managing your Cloud API key.

Using the API Key

Pass your API key in the X-API-Key header with every request:
curl -X GET "https://cloud.comfy.org/api/user" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"
const API_KEY = process.env.COMFY_CLOUD_API_KEY!;

const response = await fetch("https://cloud.comfy.org/api/user", {
  headers: { "X-API-Key": API_KEY },
});
const user = await response.json();
import os
import requests

API_KEY = os.environ["COMFY_CLOUD_API_KEY"]
headers = {"X-API-Key": API_KEY}

response = requests.get(
    "https://cloud.comfy.org/api/user",
    headers=headers
)

Core Concepts

Workflows

ComfyUI workflows are JSON objects describing a graph of nodes. The API accepts workflows in the API format (node IDs as keys with class_type, inputs, etc.) as produced by the ComfyUI frontend’s “Export Workflow (API)” option.

Jobs

When you submit a workflow, a job is created. Jobs are executed asynchronously:
  1. Submit workflow via POST /api/prompt
  2. Receive a prompt_id (job ID)
  3. Monitor progress via WebSocket or poll for status
  4. Retrieve outputs when complete

Parallel Execution (Concurrent Jobs)

API users can submit multiple workflows concurrently without waiting for previous jobs to complete. Simply send multiple POST /api/prompt requests — no special headers or parameters are needed. The dispatcher will run them in parallel up to your subscription tier’s limit.
Subscription TierConcurrent Jobs
Standard1
Creator3
Pro5
Jobs submitted beyond your concurrency limit will queue normally and execute automatically as slots free up.
Parallel execution is currently available via the API only. See pricing plans for subscription details.

Example: Submitting Multiple Jobs in Parallel

import os
import json
import asyncio
import aiohttp

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

async def submit_workflow(session, workflow):
    """Submit a single workflow and return the prompt_id."""
    async with session.post(
        f"{BASE_URL}/api/prompt",
        headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
        json={"prompt": workflow},
    ) as response:
        result = await response.json()
        return result["prompt_id"]

async def main():
    with open("workflow_api.json") as f:
        base_workflow = json.load(f)

    # Create variations by changing the seed
    workflows = []
    for seed in [42, 123, 456]:
        workflow = json.loads(json.dumps(base_workflow))
        workflow["3"]["inputs"]["seed"] = seed
        workflows.append(workflow)

    # Submit all workflows concurrently
    async with aiohttp.ClientSession() as session:
        prompt_ids = await asyncio.gather(
            *[submit_workflow(session, wf) for wf in workflows]
        )

    for pid in prompt_ids:
        print(f"Job submitted: {pid}")

    # Poll or use WebSocket to monitor each job...

asyncio.run(main())
const BASE_URL = "https://cloud.comfy.org";
const API_KEY = process.env.COMFY_CLOUD_API_KEY!;

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

async function main() {
  const base = JSON.parse(
    await Deno.readTextFile("workflow_api.json")
  );

  // Create variations by changing the seed
  const seeds = [42, 123, 456];
  const workflows = seeds.map((seed) => {
    const wf = structuredClone(base);
    wf["3"].inputs.seed = seed;
    return wf;
  });

  // Submit all workflows concurrently
  const promptIds = await Promise.all(
    workflows.map((wf) => submitWorkflow(wf))
  );

  for (const pid of promptIds) {
    console.log(`Job submitted: ${pid}`);
  }

  // Poll or use WebSocket to monitor each job...
}

main();

Outputs

Generated content (images, videos, audio) is stored in cloud storage. Output files can be downloaded via the /api/view endpoint, which returns a 302 redirect to a temporary signed URL.

Quick Start

Here’s a complete example showing how to submit a workflow, monitor its progress, and retrieve outputs:

Step 1: Submit a Workflow

curl -X POST "https://cloud.comfy.org/api/prompt" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": '"$(cat workflow_api.json)"'}'
const BASE_URL = "https://cloud.comfy.org";
const API_KEY = process.env.COMFY_CLOUD_API_KEY!;

// Load your workflow (exported from ComfyUI in API format)
const workflow = JSON.parse(await Deno.readTextFile("workflow_api.json"));

// Submit the workflow
const response = await fetch(`${BASE_URL}/api/prompt`, {
  method: "POST",
  headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ prompt: workflow }),
});
const result = await response.json();
const promptId = result.prompt_id;
console.log(`Job submitted: ${promptId}`);
import os
import requests
import json

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"}

# Load your workflow (exported from ComfyUI in API format)
with open("workflow_api.json") as f:
    workflow = json.load(f)

# Submit the workflow
response = requests.post(
    f"{BASE_URL}/api/prompt",
    headers=get_headers(),
    json={"prompt": workflow}
)
result = response.json()
prompt_id = result["prompt_id"]
print(f"Job submitted: {prompt_id}")

Step 2: Monitor Job Progress

You can monitor job completion using either polling or WebSocket for real-time updates.

Option A: Polling (Simple)

Job Status Values: The API returns one of the following status values:
StatusDescription
pendingJob is queued and waiting to start
in_progressJob is currently executing
completedJob finished successfully
failedJob encountered an error
cancelledJob was cancelled by user
# Poll for job completion
curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

# Response examples:
# {"status": "pending"}      - Job is queued
# {"status": "in_progress"}  - Job is currently running
# {"status": "completed"}    - Job finished successfully
# {"status": "failed"}       - Job encountered an error
# {"status": "cancelled"}    - Job was cancelled
interface JobStatus {
  status: string;
}

async function getJobStatus(promptId: string): Promise<JobStatus> {
  const response = await fetch(`${BASE_URL}/api/job/${promptId}/status`, {
    headers: getHeaders(),
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

async function pollForCompletion(
  promptId: string,
  timeout: number = 300,
  pollInterval: number = 2000
): Promise<void> {
  const startTime = Date.now();

  while (Date.now() - startTime < timeout * 1000) {
    const { status } = await getJobStatus(promptId);

    if (status === "completed") {
      return;
    } else if (["failed", "cancelled"].includes(status)) {
      throw new Error(`Job failed with status: ${status}`);
    }

    await new Promise((resolve) => setTimeout(resolve, pollInterval));
  }

  throw new Error(`Job ${promptId} did not complete within ${timeout}s`);
}

await pollForCompletion(promptId);
console.log("Job completed!");
def get_job_status(prompt_id: str) -> str:
    """Get the current status of a job."""
    response = requests.get(
        f"{BASE_URL}/api/job/{prompt_id}/status",
        headers=get_headers()
    )
    response.raise_for_status()
    return response.json()["status"]

def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> None:
    """Poll until job completes or times out."""
    start_time = time.time()

    while time.time() - start_time < timeout:
        status = get_job_status(prompt_id)

        if status == "completed":
            return
        elif status in ("failed", "cancelled"):
            raise RuntimeError(f"Job failed with status: {status}")

        time.sleep(poll_interval)

    raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s")

poll_for_completion(prompt_id)
print("Job completed!")

Option B: WebSocket (Real-time Progress)

For real-time progress updates and to collect output metadata:
async function listenForCompletion(
  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 did not complete within ${timeout / 1000}s`));
    }, timeout);

    ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      const msgType = data.type;
      const msgData = data.data ?? {};

      // Filter to our job
      if (msgData.prompt_id !== promptId) return;

      if (msgType === "executing") {
        const node = msgData.node;
        if (node) {
          console.log(`Executing node: ${node}`);
        } else {
          console.log("Execution complete");
        }
      } else 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") {
        console.log("Job completed successfully!");
        clearTimeout(timer);
        ws.close();
        resolve(outputs);
      } else if (msgType === "execution_error") {
        const errorMsg = msgData.exception_message ?? "Unknown error";
        clearTimeout(timer);
        ws.close();
        reject(new Error(`Execution error: ${errorMsg}`));
      }
    };

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

// Wait for completion and collect outputs
const outputs = await listenForCompletion(promptId);
import asyncio
import aiohttp
import json
import uuid

async def listen_for_completion(prompt_id: str, timeout: float = 300.0) -> dict:
    """Connect to WebSocket and listen for job completion.

    Returns:
        Final outputs from the job
    """
    ws_url = BASE_URL.replace("https://", "wss://")
    client_id = str(uuid.uuid4())
    ws_url = f"{ws_url}/ws?clientId={client_id}&token={API_KEY}"

    outputs = {}

    async with aiohttp.ClientSession() as session:
        async with session.ws_connect(ws_url) as ws:
            async def receive_messages():
                async for msg in ws:
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        data = json.loads(msg.data)
                        msg_type = data.get("type")
                        msg_data = data.get("data", {})

                        # Filter to our job
                        if msg_data.get("prompt_id") != prompt_id:
                            continue

                        if msg_type == "executing":
                            node = msg_data.get("node")
                            if node:
                                print(f"Executing node: {node}")

                        elif msg_type == "progress":
                            value = msg_data.get("value", 0)
                            max_val = msg_data.get("max", 100)
                            print(f"Progress: {value}/{max_val}")

                        elif msg_type == "executed":
                            node_id = msg_data.get("node")
                            output = msg_data.get("output", {})
                            if output:
                                outputs[node_id] = output

                        elif msg_type == "execution_success":
                            print("Job completed successfully!")
                            return outputs

                        elif msg_type == "execution_error":
                            error_msg = msg_data.get("exception_message", "Unknown error")
                            raise RuntimeError(f"Execution error: {error_msg}")

                    elif msg.type == aiohttp.WSMsgType.ERROR:
                        raise RuntimeError(f"WebSocket error: {ws.exception()}")

            try:
                return await asyncio.wait_for(receive_messages(), timeout=timeout)
            except asyncio.TimeoutError:
                raise TimeoutError(f"Job did not complete within {timeout}s")

# Wait for completion and collect outputs
outputs = await listen_for_completion(prompt_id)
See WebSocket Reference for detailed message types and handling binary preview images.

Step 3: Download Outputs

Once the job completes, download the generated files. The outputs object returned from WebSocket (or available via the history endpoint) contains output data organized by node ID. Each node’s output may contain images, video, or audio arrays with file metadata. Example outputs structure:
{
  "9": {
    "images": [
      {
        "filename": "ComfyUI_00001_.png",
        "subfolder": "",
        "type": "output"
      }
    ]
  }
}
The node ID ("9" in this example) corresponds to the SaveImage or other output nodes in your workflow. You can find these IDs by opening your workflow JSON file and looking for nodes with class_type like SaveImage, VHS_VideoCombine, etc.
# Download a single output file (follow 302 redirect with -L)
curl -L "$BASE_URL/api/view?filename=output.png&subfolder=&type=output" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -o output.png
async function downloadOutput(
  filename: string,
  subfolder: string = "",
  outputType: string = "output"
): Promise<ArrayBuffer> {
  const params = new URLSearchParams({ filename, subfolder, type: outputType });
  // Get the redirect URL
  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
  const fileResponse = await fetch(signedUrl);
  if (!fileResponse.ok) throw new Error(`HTTP ${fileResponse.status}`);
  return fileResponse.arrayBuffer();
}

async function saveOutputs(
  outputs: Record<string, any>,
  outputDir: string = "."
): Promise<void> {
  for (const nodeOutputs of Object.values(outputs)) {
    for (const key of ["images", "video", "audio"]) {
      for (const fileInfo of (nodeOutputs as any)[key] ?? []) {
        const data = await downloadOutput(
          fileInfo.filename,
          fileInfo.subfolder ?? "",
          fileInfo.type ?? "output"
        );
        const path = `${outputDir}/${fileInfo.filename}`;
        await writeFile(path, Buffer.from(data));
        console.log(`Saved: ${path}`);
      }
    }
  }
}

// Download all outputs
await saveOutputs(outputs, "./my_outputs");
def download_output(filename: str, subfolder: str = "", output_type: str = "output") -> bytes:
    """Download an output file.

    Args:
        filename: Name of the file
        subfolder: Subfolder path (usually empty)
        output_type: "output" for final outputs, "temp" for previews

    Returns:
        File bytes
    """
    params = {
        "filename": filename,
        "subfolder": subfolder,
        "type": output_type
    }

    response = requests.get(
        f"{BASE_URL}/api/view",
        headers=get_headers(),
        params=params
    )
    response.raise_for_status()
    return response.content

def save_outputs(outputs: dict, output_dir: str = "."):
    """Save all outputs from a job to disk.

    Args:
        outputs: Outputs dict from job (node_id -> output_data)
        output_dir: Directory to save files to
    """
    import os
    os.makedirs(output_dir, exist_ok=True)

    for node_id, node_outputs in outputs.items():
        for key in ("images", "video", "audio"):
            for file_info in node_outputs.get(key, []):
                filename = file_info["filename"]
                subfolder = file_info.get("subfolder", "")
                output_type = file_info.get("type", "output")

                data = download_output(filename, subfolder, output_type)

                output_path = os.path.join(output_dir, filename)
                with open(output_path, "wb") as f:
                    f.write(data)
                print(f"Saved: {output_path}")

# Download all outputs
save_outputs(outputs, "./my_outputs")
The /api/view endpoint returns a 302 redirect to a temporary signed URL. Your HTTP client must follow redirects to download the file.

Complete Example

Here’s a full end-to-end example combining all three steps:
import { readFile, writeFile } from "fs/promises";

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

async function main() {
  // 1. Load and modify workflow
  const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8"));
  workflow["3"].inputs.seed = 42;
  workflow["6"].inputs.text = "a beautiful sunset";

  // 2. Submit workflow
  const response = await fetch(`${BASE_URL}/api/prompt`, {
    method: "POST",
    headers: { "X-API-Key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ prompt: workflow }),
  });
  const { prompt_id } = await response.json();
  console.log(`Job submitted: ${prompt_id}`);

  // 3. Poll for completion
  while (true) {
    const statusRes = await fetch(`${BASE_URL}/api/job/${prompt_id}/status`, {
      headers: { "X-API-Key": API_KEY },
    });
    const { status } = await statusRes.json();

    if (status === "completed") break;
    if (["failed", "cancelled"].includes(status)) {
      throw new Error(`Job ${status}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 2000));
  }

  // 4. Get outputs via job detail endpoint
  const jobRes = await fetch(`${BASE_URL}/api/jobs/${prompt_id}`, {
    headers: { "X-API-Key": API_KEY },
  });
  const job = await jobRes.json();
  const outputs = job.outputs;

  // 5. Download output files
  for (const nodeOutputs of Object.values(outputs)) {
    for (const fileInfo of (nodeOutputs as any).images ?? []) {
      const params = new URLSearchParams({
        filename: fileInfo.filename,
        subfolder: fileInfo.subfolder ?? "",
        type: "output",
      });
      const viewRes = await fetch(`${BASE_URL}/api/view?${params}`, {
        headers: { "X-API-Key": API_KEY },
        redirect: "manual",
      });
      const signedUrl = viewRes.headers.get("location")!;
      const fileRes = await fetch(signedUrl);
      await writeFile(`./${fileInfo.filename}`, Buffer.from(await fileRes.arrayBuffer()));
      console.log(`Downloaded: ${fileInfo.filename}`);
    }
  }
}

main();
import os
import requests
import json
import time

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

def main():
    # 1. Load and modify workflow
    with open("workflow_api.json") as f:
        workflow = json.load(f)

    workflow["3"]["inputs"]["seed"] = 42
    workflow["6"]["inputs"]["text"] = "a beautiful sunset"

    # 2. Submit workflow
    response = requests.post(
        f"{BASE_URL}/api/prompt",
        headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
        json={"prompt": workflow}
    )
    prompt_id = response.json()["prompt_id"]
    print(f"Job submitted: {prompt_id}")

    # 3. Poll for completion
    while True:
        status_res = requests.get(
            f"{BASE_URL}/api/job/{prompt_id}/status",
            headers={"X-API-Key": API_KEY}
        )
        status = status_res.json()["status"]

        if status == "completed":
            break
        if status in ("failed", "cancelled"):
            raise RuntimeError(f"Job {status}")
        time.sleep(2)

    # 4. Get outputs via job detail endpoint
    job_res = requests.get(
        f"{BASE_URL}/api/jobs/{prompt_id}",
        headers={"X-API-Key": API_KEY}
    )
    job = job_res.json()
    outputs = job["outputs"]

    # 5. Download output files
    for node_outputs in outputs.values():
        for file_info in node_outputs.get("images", []):
            params = {
                "filename": file_info["filename"],
                "subfolder": file_info.get("subfolder", ""),
                "type": "output"
            }
            view_res = requests.get(
                f"{BASE_URL}/api/view",
                headers={"X-API-Key": API_KEY},
                params=params
            )
            with open(file_info["filename"], "wb") as f:
                f.write(view_res.content)
            print(f"Downloaded: {file_info['filename']}")

if __name__ == "__main__":
    main()

Available Endpoints

CategoryDescription
WorkflowsSubmit workflows, check status
JobsMonitor job status and queue
InputsUpload images, masks, and other inputs
OutputsDownload generated content
WebSocketReal-time progress updates
Object InfoAvailable nodes and their definitions

Next Steps

The quick start above covers the basics of submitting workflows and retrieving results. For more advanced use cases, refer to the Cloud API Reference:
  • Uploading Input Files - Upload images, masks, or other user-provided content for workflows that require external inputs
  • Modifying Workflow Inputs - Dynamically change workflow parameters like prompts, seeds, or node settings before submission
  • Using Partner Nodes - Call external AI services (Flux Pro, Ideogram, etc.) that require additional API key configuration
  • Queue Management - Monitor queue status, cancel jobs, or interrupt running executions
  • Error Handling - Handle HTTP errors, execution failures, and understand exception types
Additional resources: