跳转到主要内容
实验性 API: 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。部分端点为兼容本地 ComfyUI 而保留,但可能具有不同的语义(例如,某些字段会被忽略)。

Cloud API 参考

本页面提供了常见 Comfy Cloud API 操作的完整示例。
需要订阅: 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。请查看定价方案了解详情。

设置

所有示例都使用以下通用的导入和配置:
export COMFY_CLOUD_API_KEY="your-api-key"
export BASE_URL="https://cloud.comfy.org"

对象信息

获取可用的节点定义。这对于了解可用的节点及其输入/输出规范非常有用。
curl -X GET "$BASE_URL/api/object_info" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

上传输入

上传图像、遮罩或其他文件以在工作流中使用。

直接上传(Multipart)

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"

上传遮罩

subfolder 参数为 API 兼容性而接受,但在云存储中会被忽略。所有文件都存储在扁平的、内容寻址的命名空间中。
curl -X POST "$BASE_URL/api/upload/mask" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -F "[email protected]" \
  -F "type=input" \
  -F "subfolder=clipspace" \
  -F 'original_ref={"filename":"my_image.png","subfolder":"","type":"input"}'

运行工作流

提交工作流以执行。

提交工作流

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

使用合作伙伴节点

如果您的工作流包含合作伙伴节点(调用外部 AI 服务的节点,如 Flux Pro、Ideogram 等),您必须在请求体的 extra_data 字段中包含您的 Comfy API 密钥。
在浏览器中运行工作流时,ComfyUI 前端会自动将您的 API 密钥打包到 extra_data 中。本节仅适用于直接调用 API 的情况。
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"
    }
  }'
platform.comfy.org 生成您的 API 密钥。此密钥与 Cloud API 身份验证(X-API-Key 请求头)使用的是同一个密钥。

修改工作流输入

function setWorkflowInput(
  workflow: Record<string, any>,
  nodeId: string,
  inputName: string,
  value: any
): Record<string, any> {
  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");

检查任务状态

轮询任务完成状态。
curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

实时进度 WebSocket

连接 WebSocket 以获取实时执行更新。
clientId 参数目前会被忽略——同一用户的所有连接都会收到相同的消息。为了向前兼容,仍建议传递唯一的 clientId
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";
        const nodeType = msgData.node_type ?? "";
        clearTimeout(timer);
        ws.close();
        reject(new Error(`Execution error in ${nodeType}: ${errorMsg}`));
      }
    };

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

// Usage
const promptId = await submitWorkflow(workflow);
const outputs = await listenForCompletion(promptId);

WebSocket 消息类型

消息以 JSON 文本帧的形式发送,除非另有说明。
类型描述
status队列状态更新,包含 queue_remaining 计数
notification用户友好的状态消息(value 字段包含如 “Executing workflow…” 的文本)
execution_start工作流执行已开始
executing特定节点正在执行(节点 ID 在 node 字段中)
progress节点内的步骤进度(采样步骤的 value/max
progress_state扩展进度状态,包含节点元数据(嵌套的 nodes 对象)
executed节点完成并输出结果(图像、视频等在 output 字段中)
execution_cached因输出已缓存而跳过的节点(nodes 数组)
execution_success整个工作流成功完成
execution_error工作流失败(包含 exception_typeexception_messagetraceback
execution_interrupted工作流被用户取消

二进制消息(预览图像)

在图像生成过程中,ComfyUI 会发送包含预览图像的二进制 WebSocket 帧。这些是原始二进制数据(不是 JSON):
二进制类型描述
PREVIEW_IMAGE1扩散采样期间的进度预览
TEXT3节点的文本输出(进度文本)
PREVIEW_IMAGE_WITH_METADATA4带有节点上下文元数据的预览图像
二进制帧格式(所有整数为大端序):
偏移大小字段描述
04 字节type0x00000001
44 字节image_type格式代码(1=JPEG, 2=PNG)
8可变image_data原始图像字节
请参阅 OpenAPI 规范 了解每种 JSON 消息类型的完整模式定义。

下载输出

在任务完成后检索生成的文件。
# 下载单个输出文件(使用 -L 跟随 302 重定向)
curl -L "$BASE_URL/api/view?filename=output.png&subfolder=&type=output" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -o output.png

完整端到端示例

以下是一个将所有内容整合在一起的完整示例:
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<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}`);
  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<void> {
  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();

队列管理

获取队列状态

curl -X GET "$BASE_URL/api/queue" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

取消任务

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

中断当前执行

curl -X POST "$BASE_URL/api/interrupt" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

错误处理

HTTP 错误

REST API 端点返回标准 HTTP 状态码:
状态码描述
400无效请求(错误的工作流、缺少字段)
401未授权(无效或缺少 API 密钥)
402余额不足
429订阅未激活
500内部服务器错误

执行错误

在工作流执行期间,错误通过 execution_error WebSocket 消息传递。exception_type 字段标识错误类别:
异常类型描述
ValidationError无效的工作流或输入
ModelDownloadError所需模型不可用或下载失败
ImageDownloadError从 URL 下载输入图像失败
OOMErrorGPU 内存不足
InsufficientFundsError账户余额不足(用于合作伙伴节点)
InactiveSubscriptionError订阅未激活