跳转到主要内容
实验性 API: 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。

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"

上传遮罩

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

引用已知资源(跳过上传)

如果您知道某个文件已存在于云存储中(例如,Comfy 共享的热门示例图像),您可以创建资源引用而无需上传任何字节。首先检查哈希是否存在,然后创建您自己的引用。
# 1. Check if the hash exists (unauthenticated)
# Hash format: blake3:<64 hex chars>
curl -I "$BASE_URL/api/assets/hash/blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"
# Returns 200 if exists, 404 if not

# 2. Create your own asset reference pointing to that hash
curl -X POST "$BASE_URL/api/assets/from-hash" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "hash": "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
    "tags": ["input"],
    "name": "sample_portrait.png"
  }'

运行工作流

提交工作流以执行。

提交工作流

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

修改工作流输入

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");

检查任务状态

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

实时进度 WebSocket

连接 WebSocket 以获取实时执行更新。
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 计数
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 消息类型的完整模式定义。

下载输出

在任务完成后检索生成的文件。
# Download a single output file
curl -X GET "$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",
        });
        const response = await fetch(`${BASE_URL}/api/view?${params}`, {
          headers: getHeaders(),
        });
        if (!response.ok) throw new Error(`HTTP ${response.status}`);

        const path = `${outputDir}/${fileInfo.filename}`;
        await writeFile(path, Buffer.from(await response.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"

错误处理

API 通过 execution_error WebSocket 消息或 HTTP 错误响应返回结构化错误。exception_type 字段标识错误类别:
异常类型描述
ValidationError无效的工作流或输入
ModelDownloadError所需模型不可用或下载失败
ImageDownloadError从 URL 下载输入图像失败
OOMErrorGPU 内存不足
PanicError执行期间发生意外服务器崩溃
ServiceError内部服务错误
WebSocketErrorWebSocket 连接或通信错误
DispatcherError任务分发或路由错误
InsufficientFundsError账户余额不足
InactiveSubscriptionError订阅未激活
async function handleApiError(response: Response): Promise<never> {
  if (response.status === 402) {
    throw new Error("Insufficient credits. Please add funds to your account.");
  } else if (response.status === 429) {
    throw new Error("Rate limited or subscription inactive.");
  } else if (response.status >= 400) {
    try {
      const error = await response.json();
      throw new Error(`API error: ${error.message ?? response.statusText}`);
    } catch {
      throw new Error(`API error: ${response.statusText}`);
    }
  }
  throw new Error("Unknown error");
}

// Usage
const response = await fetch(`${BASE_URL}/api/prompt`, {
  method: "POST",
  headers: getHeaders(),
  body: JSON.stringify({ prompt: workflow }),
});
if (!response.ok) {
  await handleApiError(response);
}