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).
Cloud API Reference
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 for details.
Setup
All examples use these common imports and configuration:
export COMFY_CLOUD_API_KEY="your-api-key"
export BASE_URL="https://cloud.comfy.org"
Object Info
Retrieve available node definitions. This is useful for understanding what nodes are available and their input/output specifications.
curl -X GET "$BASE_URL/api/object_info" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
Upload images, masks, or other files for use in workflows.
Direct Upload (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"
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.
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"}'
Running Workflows
Submit a workflow for execution.
Submit Workflow
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)"'}'
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.
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"
}
}'
Generate your API key at platform.comfy.org. This is the same key used for Cloud API authentication (X-API-Key header).
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");
Checking Job Status
Poll for job completion.
curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
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.
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 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:{
"node_id": "3",
"display_node_id": "3",
"real_node_id": "3",
"prompt_id": "abc-123",
"parent_node_id": null
}
Downloading Outputs
Retrieve generated files after job completion.
# 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
Complete End-to-End Example
Here’s a full example that ties everything together:
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();
Queue Management
Get Queue Status
curl -X GET "$BASE_URL/api/queue" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
Cancel a Job
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"]}'
Interrupt Current Execution
curl -X POST "$BASE_URL/api/interrupt" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"
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 |