实验性 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"
}
}'
修改工作流输入
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_type、exception_message、traceback) |
execution_interrupted | 工作流被用户取消 |
二进制消息(预览图像)
在图像生成过程中,ComfyUI 会发送包含预览图像的二进制 WebSocket 帧。这些是原始二进制数据(不是 JSON):
| 二进制类型 | 值 | 描述 |
|---|
PREVIEW_IMAGE | 1 | 扩散采样期间的进度预览 |
TEXT | 3 | 节点的文本输出(进度文本) |
PREVIEW_IMAGE_WITH_METADATA | 4 | 带有节点上下文元数据的预览图像 |
二进制帧格式(所有整数为大端序):
| 偏移 | 大小 | 字段 | 描述 |
|---|
| 0 | 4 字节 | type | 0x00000001 |
| 4 | 4 字节 | image_type | 格式代码(1=JPEG, 2=PNG) |
| 8 | 可变 | image_data | 原始图像字节 |
| 偏移 | 大小 | 字段 | 描述 |
|---|
| 0 | 4 字节 | type | 0x00000003 |
| 4 | 4 字节 | node_id_len | node_id 字符串的长度 |
| 8 | N 字节 | node_id | UTF-8 编码的节点 ID |
| 8+N | 可变 | text | UTF-8 编码的进度文本 |
| 偏移 | 大小 | 字段 | 描述 |
|---|
| 0 | 4 字节 | type | 0x00000004 |
| 4 | 4 字节 | metadata_len | 元数据 JSON 的长度 |
| 8 | N 字节 | metadata | UTF-8 JSON(见下文) |
| 8+N | 可变 | image_data | 原始 JPEG/PNG 字节 |
元数据 JSON 结构:{
"node_id": "3",
"display_node_id": "3",
"real_node_id": "3",
"prompt_id": "abc-123",
"parent_node_id": null
}
下载输出
在任务完成后检索生成的文件。
# 下载单个输出文件(使用 -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 下载输入图像失败 |
OOMError | GPU 内存不足 |
InsufficientFundsError | 账户余额不足(用于合作伙伴节点) |
InactiveSubscriptionError | 订阅未激活 |