From 5353c5c5ae290a1f7c2f321f5513f31310e8afd8 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Wed, 31 Dec 2025 17:29:12 -0500 Subject: [PATCH 01/27] Add Cloud API documentation with examples and OpenAPI spec - Add api-reference.mdx with code examples (curl, TypeScript, Python) - Add overview.mdx explaining authentication, core concepts, workflows - Add openapi.mdx integrating OpenAPI specification - Add openapi-cloud.yaml with full API spec (workflows, assets, jobs, WebSocket) - Update navigation to include new Cloud API section Fixes from review: - Use correct hash format (blake3:<64 hex chars>) in examples - Fix Python WebSocket async iteration bug - Use Node.js fs/promises instead of Deno APIs - Align on 'Experimental API' terminology across all files - Document hash format differences between endpoints --- development/cloud/api-reference.mdx | 1314 ++++++++++ development/cloud/openapi.mdx | 72 + development/cloud/overview.mdx | 177 ++ development/overview.mdx | 3 +- docs.json | 16 + get_started/cloud.mdx | 4 + index.mdx | 11 +- openapi-cloud.yaml | 3699 +++++++++++++++++++++++++++ 8 files changed, 5293 insertions(+), 3 deletions(-) create mode 100644 development/cloud/api-reference.mdx create mode 100644 development/cloud/openapi.mdx create mode 100644 development/cloud/overview.mdx create mode 100644 openapi-cloud.yaml diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx new file mode 100644 index 000000000..b54c406e5 --- /dev/null +++ b/development/cloud/api-reference.mdx @@ -0,0 +1,1314 @@ +--- +title: "Cloud API Reference" +description: "Complete API reference with code examples for ComfyUI Cloud" +--- + + + **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. + + +# Cloud API Reference + +This page provides complete examples for common ComfyUI Cloud API operations. + + + **Subscription Required:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + + +## Setup + +All examples use these common imports and configuration: + + +```bash curl +export COMFY_CLOUD_API_KEY="your-api-key" +export BASE_URL="https://cloud.comfy.org" +``` + +```typescript TypeScript +import { readFile, writeFile } from "fs/promises"; + +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", + }; +} +``` + +```python Python +import os +import requests +import json +import time +import asyncio +import aiohttp + +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" + } +``` + + +--- + +## Object Info + +Retrieve available node definitions. This is useful for understanding what nodes are available and their input/output specifications. + + +```bash curl +curl -X GET "$BASE_URL/api/object_info" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function getObjectInfo(): Promise> { + const response = await fetch(`${BASE_URL}/api/object_info`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const objectInfo = await getObjectInfo(); +console.log(`Available nodes: ${Object.keys(objectInfo).length}`); + +const ksampler = objectInfo["KSampler"] ?? {}; +console.log(`KSampler inputs: ${Object.keys(ksampler.input?.required ?? {})}`); +``` + +```python Python +def get_object_info(): + """Fetch all available node definitions from cloud.""" + response = requests.get( + f"{BASE_URL}/api/object_info", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +# Get all nodes +object_info = get_object_info() +print(f"Available nodes: {len(object_info)}") + +# Get a specific node's definition +ksampler = object_info.get("KSampler", {}) +print(f"KSampler inputs: {list(ksampler.get('input', {}).get('required', {}).keys())}") +``` + + +--- + +## Uploading Inputs + +Upload images, masks, or other files for use in workflows. + +### Direct Upload (Multipart) + + +```bash curl +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" +``` + +```typescript TypeScript +async function uploadInput( + filePath: string, + inputType: string = "input" +): Promise<{ name: string; subfolder: string }> { + const file = await readFile(filePath); + const formData = new FormData(); + formData.append("image", new Blob([file]), filePath.split("/").pop()); + formData.append("type", inputType); + formData.append("overwrite", "true"); + + const response = await fetch(`${BASE_URL}/api/upload/image`, { + method: "POST", + headers: { "X-API-Key": API_KEY }, + body: formData, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const result = await uploadInput("my_image.png"); +console.log(`Uploaded: ${result.name} to ${result.subfolder}`); +``` + +```python Python +def upload_input(file_path: str, input_type: str = "input") -> dict: + """Upload a file directly to cloud. + + Args: + file_path: Path to the file to upload + input_type: "input" for images, "temp" for temporary files + + Returns: + Upload response with filename and subfolder + """ + with open(file_path, "rb") as f: + files = {"image": f} + data = {"type": input_type, "overwrite": "true"} + + response = requests.post( + f"{BASE_URL}/api/upload/image", + headers={"X-API-Key": API_KEY}, # No Content-Type for multipart + files=files, + data=data + ) + response.raise_for_status() + return response.json() + +# Upload an image +result = upload_input("my_image.png") +print(f"Uploaded: {result['name']} to {result['subfolder']}") +``` + + +### Upload Mask + + +```bash curl +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"}' +``` + +```typescript TypeScript +async function uploadMask( + filePath: string, + originalRef: { filename: string; subfolder: string; type: string } +): Promise<{ name: string; subfolder: string }> { + const file = await readFile(filePath); + const formData = new FormData(); + formData.append("image", new Blob([file]), filePath.split("/").pop()); + formData.append("type", "input"); + formData.append("subfolder", "clipspace"); + formData.append("original_ref", JSON.stringify(originalRef)); + + const response = await fetch(`${BASE_URL}/api/upload/mask`, { + method: "POST", + headers: { "X-API-Key": API_KEY }, + body: formData, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const maskResult = await uploadMask("mask.png", { + filename: "my_image.png", + subfolder: "", + type: "input", +}); +console.log(`Uploaded mask: ${maskResult.name}`); +``` + +```python Python +def upload_mask(file_path: str, original_ref: dict) -> dict: + """Upload a mask associated with an original image. + + Args: + file_path: Path to the mask file + original_ref: Reference to the original image {"filename": "...", "subfolder": "...", "type": "..."} + """ + with open(file_path, "rb") as f: + files = {"image": f} + data = { + "type": "input", + "subfolder": "clipspace", + "original_ref": json.dumps(original_ref) + } + + response = requests.post( + f"{BASE_URL}/api/upload/mask", + headers={"X-API-Key": API_KEY}, + files=files, + data=data + ) + response.raise_for_status() + return response.json() +``` + + +### Reference Well-Known Assets (Skip Upload) + +If you know a file already exists in cloud storage (e.g., a popular sample image shared by Comfy), you can create an asset reference without uploading any bytes. First check if the hash exists, then create your own reference to it. + + +```bash curl +# 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" + }' +``` + +```typescript TypeScript +async function checkHashExists(hash: string): Promise { + // Note: /api/assets/hash/{hash} only accepts blake3: format + const response = await fetch(`${BASE_URL}/api/assets/hash/${hash}`, { + method: "HEAD", + }); + return response.ok; +} + +async function createAssetFromHash( + hash: string, + name: string, + tags: string[] = ["input"] +): Promise<{ id: string; hash: string }> { + // Note: /api/assets/from-hash accepts both blake3: and sha256: formats + const response = await fetch(`${BASE_URL}/api/assets/from-hash`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ hash, name, tags }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +// Use a well-known sample image without uploading +// Hash format: blake3:<64 hex chars> +const sampleHash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"; + +if (await checkHashExists(sampleHash)) { + const asset = await createAssetFromHash(sampleHash, "sample_portrait.png"); + console.log(`Created asset ${asset.id} pointing to ${asset.hash}`); + + // Use in workflow (assuming workflow is already loaded) + // workflow["1"]["inputs"]["image"] = asset.hash; +} +``` + +```python Python +def check_hash_exists(hash: str) -> bool: + """Check if a file hash exists in cloud storage (unauthenticated). + + Note: This endpoint only accepts blake3: format hashes. + """ + response = requests.head(f"{BASE_URL}/api/assets/hash/{hash}") + return response.status_code == 200 + +def create_asset_from_hash(hash: str, name: str, tags: list = None) -> dict: + """Create an asset reference from an existing hash. + + This skips uploading bytes entirely - useful for well-known files + or files you've previously uploaded to the cloud. + + Note: This endpoint accepts both blake3: and sha256: format hashes. + """ + response = requests.post( + f"{BASE_URL}/api/assets/from-hash", + headers=get_headers(), + json={ + "hash": hash, + "name": name, + "tags": tags or ["input"] + } + ) + response.raise_for_status() + return response.json() + +# Use a well-known sample image without uploading +# Hash format: blake3:<64 hex chars> or sha256:<64 hex chars> +sample_hash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" + +if check_hash_exists(sample_hash): + asset = create_asset_from_hash(sample_hash, "sample_portrait.png") + print(f"Created asset {asset['id']} pointing to {asset['hash']}") + + # Use in workflow + workflow["1"]["inputs"]["image"] = asset["hash"] +``` + + +--- + +## Running Workflows + +Submit a workflow for execution. + +### Submit Workflow + + +```bash curl +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)"'}' +``` + +```typescript TypeScript +async function submitWorkflow(workflow: Record): Promise { + 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}`); + const result = await response.json(); + + if (result.error) { + throw new Error(`Workflow error: ${result.error}`); + } + return result.prompt_id; +} + +const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8")); +const promptId = await submitWorkflow(workflow); +console.log(`Submitted job: ${promptId}`); +``` + +```python Python +def submit_workflow(workflow: dict) -> str: + """Submit a workflow and return the prompt_id (job ID). + + Args: + workflow: ComfyUI workflow in API format + + Returns: + prompt_id for tracking the job + """ + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={"prompt": workflow} + ) + response.raise_for_status() + result = response.json() + + if "error" in result: + raise ValueError(f"Workflow error: {result['error']}") + + return result["prompt_id"] + +# Load and submit a workflow +with open("workflow_api.json") as f: + workflow = json.load(f) + +prompt_id = submit_workflow(workflow) +print(f"Submitted job: {prompt_id}") +``` + + +### Modify Workflow Inputs + + +```typescript TypeScript +function setWorkflowInput( + workflow: Record, + nodeId: string, + inputName: string, + value: any +): Record { + 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"); +``` + +```python Python +def set_workflow_input(workflow: dict, node_id: str, input_name: str, value) -> dict: + """Modify a workflow input value. + + Args: + workflow: The workflow dict + node_id: ID of the node to modify + input_name: Name of the input field + value: New value + + Returns: + Modified workflow + """ + if node_id in workflow: + workflow[node_id]["inputs"][input_name] = value + return workflow + +# Example: Set seed and prompt +workflow = set_workflow_input(workflow, "3", "seed", 12345) +workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") +``` + + +--- + +## Checking Job Status + +Poll for job completion status. + + +```bash curl +# Get job status +curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +interface JobStatus { + status: string; + outputs?: Record; +} + +async function getJobStatus(promptId: string): Promise { + 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 { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout * 1000) { + const status = await getJobStatus(promptId); + + if (status.status === "completed") { + return status; + } else if (["error", "failed", "cancelled"].includes(status.status)) { + throw new Error(`Job failed with status: ${status.status}`); + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + throw new Error(`Job ${promptId} did not complete within ${timeout}s`); +} + +const result = await pollForCompletion(promptId); +console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); +``` + +```python Python +def get_job_status(prompt_id: str) -> dict: + """Get the status of a job. + + Returns: + Job status with fields: status, outputs (if complete) + """ + response = requests.get( + f"{BASE_URL}/api/job/{prompt_id}/status", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> dict: + """Poll until job completes or times out. + + Args: + prompt_id: The job ID + timeout: Maximum seconds to wait + poll_interval: Seconds between polls + + Returns: + Final job status + """ + start_time = time.time() + + while time.time() - start_time < timeout: + status = get_job_status(prompt_id) + job_status = status.get("status", "unknown") + + if job_status == "completed": + return status + elif job_status in ("error", "failed", "cancelled"): + raise RuntimeError(f"Job failed with status: {job_status}") + + # Still pending or in_progress + time.sleep(poll_interval) + + raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s") + +# Wait for job to complete +result = poll_for_completion(prompt_id) +print(f"Job completed! Outputs: {result.get('outputs', {}).keys()}") +``` + + +--- + +## WebSocket for Real-Time Progress + +Connect to the WebSocket for real-time execution updates. + + +```typescript TypeScript +async function listenForCompletion( + promptId: string, + timeout: number = 300000 +): Promise> { + const wsUrl = `wss://cloud.comfy.org/ws?clientId=${crypto.randomUUID()}&token=${API_KEY}`; + const outputs: Record = {}; + + 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); +``` + +```python Python +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. + + Args: + prompt_id: The job ID to monitor + timeout: Maximum seconds to wait + + Returns: + Final outputs from the job + """ + # Build WebSocket URL + ws_url = BASE_URL.replace("https://", "wss://").replace("http://", "ws://") + 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}") + else: + print("Execution complete") + + 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") + node_type = msg_data.get("node_type", "") + raise RuntimeError(f"Execution error in {node_type}: {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") + + return outputs + +# Usage +async def run_with_websocket(): + prompt_id = submit_workflow(workflow) + outputs = await listen_for_completion(prompt_id) + return outputs + +# Run async +outputs = asyncio.run(run_with_websocket()) +``` + + +### WebSocket Message Types + +Messages are sent as JSON text frames unless otherwise noted. + +| Type | Description | +|------|-------------| +| `status` | Queue status update with `queue_remaining` count | +| `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:** + ```json + { + "node_id": "3", + "display_node_id": "3", + "real_node_id": "3", + "prompt_id": "abc-123", + "parent_node_id": null + } + ``` + + + + + See the [OpenAPI Specification](/development/cloud/openapi) for complete schema definitions of each JSON message type. + + +--- + +## Downloading Outputs + +Retrieve generated files after job completion. + + +```bash curl +# 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 +``` + +```typescript TypeScript +async function downloadOutput( + filename: string, + subfolder: string = "", + outputType: string = "output" +): Promise { + const params = new URLSearchParams({ filename, subfolder, type: outputType }); + const response = await fetch(`${BASE_URL}/api/view?${params}`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.arrayBuffer(); +} + +async function saveOutputs( + outputs: Record, + outputDir: string = "." +): Promise { + 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}`); + } + } + } +} + +await saveOutputs(outputs, "./my_outputs"); +``` + +```python Python +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(): + # Handle image outputs + if "images" in node_outputs: + for img_info in node_outputs["images"]: + filename = img_info["filename"] + subfolder = img_info.get("subfolder", "") + output_type = img_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}") + + # Handle video outputs + if "video" in node_outputs: + for vid_info in node_outputs["video"]: + filename = vid_info["filename"] + subfolder = vid_info.get("subfolder", "") + output_type = vid_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}") + +# After job completes +save_outputs(outputs, "./my_outputs") +``` + + +--- + +## Complete End-to-End Example + +Here's a full example that ties everything together: + + +```typescript TypeScript +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): Promise { + 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> { + const wsUrl = `wss://cloud.comfy.org/ws?clientId=${crypto.randomUUID()}&token=${API_KEY}`; + const outputs: Record = {}; + + 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, + outputDir: string +): Promise { + 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(); +``` + +```python Python +import os +import requests +import json +import asyncio +import aiohttp +import uuid + +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"} + +def upload_image(file_path: str) -> dict: + """Upload an image and return the reference for use in workflows.""" + with open(file_path, "rb") as f: + response = requests.post( + f"{BASE_URL}/api/upload/image", + headers={"X-API-Key": API_KEY}, + files={"image": f}, + data={"type": "input", "overwrite": "true"} + ) + response.raise_for_status() + return response.json() + +def submit_workflow(workflow: dict) -> str: + """Submit workflow and return prompt_id.""" + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={"prompt": workflow} + ) + response.raise_for_status() + return response.json()["prompt_id"] + +async def wait_for_completion(prompt_id: str, timeout: float = 300.0) -> dict: + """Wait for job completion via WebSocket.""" + ws_url = BASE_URL.replace("https://", "wss://") + f"/ws?clientId={uuid.uuid4()}&token={API_KEY}" + outputs = {} + + async with aiohttp.ClientSession() as session: + async with session.ws_connect(ws_url) as ws: + start = asyncio.get_event_loop().time() + async for msg in ws: + if asyncio.get_event_loop().time() - start > timeout: + raise TimeoutError("Job timed out") + + if msg.type != aiohttp.WSMsgType.TEXT: + continue + + data = json.loads(msg.data) + if data.get("data", {}).get("prompt_id") != prompt_id: + continue + + msg_type = data.get("type") + msg_data = data.get("data", {}) + + if msg_type == "progress": + print(f"Progress: {msg_data.get('value')}/{msg_data.get('max')}") + elif msg_type == "executed": + if output := msg_data.get("output"): + outputs[msg_data["node"]] = output + elif msg_type == "execution_success": + return outputs + elif msg_type == "execution_error": + raise RuntimeError(msg_data.get("exception_message", "Unknown error")) + + return outputs + +def download_outputs(outputs: dict, output_dir: str): + """Download all output files.""" + os.makedirs(output_dir, exist_ok=True) + + for node_outputs in outputs.values(): + for key in ["images", "video", "audio"]: + for file_info in node_outputs.get(key, []): + params = { + "filename": file_info["filename"], + "subfolder": file_info.get("subfolder", ""), + "type": file_info.get("type", "output") + } + response = requests.get(f"{BASE_URL}/api/view", headers=get_headers(), params=params) + response.raise_for_status() + + path = os.path.join(output_dir, file_info["filename"]) + with open(path, "wb") as f: + f.write(response.content) + print(f"Downloaded: {path}") + +async def main(): + # 1. Load workflow + with open("workflow_api.json") as f: + workflow = json.load(f) + + # 2. Optionally upload input images + # image_ref = upload_image("input.png") + # workflow["1"]["inputs"]["image"] = image_ref["name"] + + # 3. Modify workflow parameters + workflow["3"]["inputs"]["seed"] = 42 + workflow["6"]["inputs"]["text"] = "a beautiful sunset over mountains" + + # 4. Submit workflow + prompt_id = submit_workflow(workflow) + print(f"Job submitted: {prompt_id}") + + # 5. Wait for completion with progress + outputs = await wait_for_completion(prompt_id) + print(f"Job completed! Found {len(outputs)} output nodes") + + # 6. Download outputs + download_outputs(outputs, "./outputs") + print("Done!") + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +--- + +## Queue Management + +### Get Queue Status + + +```bash curl +curl -X GET "$BASE_URL/api/queue" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function getQueue(): Promise<{ + queue_running: any[]; + queue_pending: any[]; +}> { + const response = await fetch(`${BASE_URL}/api/queue`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const queue = await getQueue(); +console.log(`Running: ${queue.queue_running.length}`); +console.log(`Pending: ${queue.queue_pending.length}`); +``` + +```python Python +def get_queue(): + """Get current queue status.""" + response = requests.get( + f"{BASE_URL}/api/queue", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +queue = get_queue() +print(f"Running: {len(queue.get('queue_running', []))}") +print(f"Pending: {len(queue.get('queue_pending', []))}") +``` + + +### Cancel a Job + + +```bash curl +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"]}' +``` + +```typescript TypeScript +async function cancelJob(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/queue`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ delete: [promptId] }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); +} +``` + +```python Python +def cancel_job(prompt_id: str): + """Cancel a pending or running job.""" + response = requests.post( + f"{BASE_URL}/api/queue", + headers=get_headers(), + json={"delete": [prompt_id]} + ) + response.raise_for_status() + return response.json() +``` + + +### Interrupt Current Execution + + +```bash curl +curl -X POST "$BASE_URL/api/interrupt" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function interrupt(): Promise { + const response = await fetch(`${BASE_URL}/api/interrupt`, { + method: "POST", + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); +} +``` + +```python Python +def interrupt(): + """Interrupt the currently running job.""" + response = requests.post( + f"{BASE_URL}/api/interrupt", + headers=get_headers() + ) + response.raise_for_status() +``` + + +--- + +## Error Handling + +The API returns structured errors via the `execution_error` WebSocket message or HTTP error responses. 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 | +| `PanicError` | Unexpected server crash during execution | +| `ServiceError` | Internal service error | +| `WebSocketError` | WebSocket connection or communication error | +| `DispatcherError` | Job dispatch or routing error | +| `InsufficientFundsError` | Account balance too low | +| `InactiveSubscriptionError` | Subscription not active | + + +```typescript TypeScript +async function handleApiError(response: Response): Promise { + 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); +} +``` + +```python Python +def handle_api_error(response): + """Handle API error responses.""" + if response.status_code == 402: + raise ValueError("Insufficient credits. Please add funds to your account.") + elif response.status_code == 429: + raise ValueError("Rate limited or subscription inactive.") + elif response.status_code >= 400: + try: + error = response.json() + raise ValueError(f"API error: {error.get('message', response.text)}") + except json.JSONDecodeError: + raise ValueError(f"API error: {response.text}") +``` + diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx new file mode 100644 index 000000000..b8e608d27 --- /dev/null +++ b/development/cloud/openapi.mdx @@ -0,0 +1,72 @@ +--- +title: "OpenAPI Specification" +description: "Machine-readable OpenAPI specification for ComfyUI Cloud API" +openapi: "/openapi-cloud.yaml" +--- + + + **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. + + +# ComfyUI Cloud API Specification + +This page provides the complete OpenAPI 3.0 specification for the ComfyUI Cloud API. + + + **Subscription Required:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + + +## Interactive API Reference + +Explore the full API with interactive documentation: + + + Browse endpoints, view schemas, and try requests + + +## Using the Specification + +The OpenAPI spec can be used to: + +- **Generate client libraries** in any language using tools like [OpenAPI Generator](https://openapi-generator.tech/) +- **Import into API tools** like Postman, Insomnia, or Paw +- **Generate documentation** using tools like Redoc or Swagger UI +- **Validate requests** in your application + +## Download + +You can download the raw OpenAPI specification file: + + + Download the OpenAPI 3.0 specification + + +## Authentication + +All API requests require an API key passed in the `X-API-Key` header. + +### Getting an API Key + +See the [Cloud API Overview](/development/cloud/overview#getting-an-api-key) for step-by-step instructions with screenshots. + +### Using the API Key + +```yaml +X-API-Key: your-api-key +``` + +## Base URL + +| Environment | URL | +|-------------|-----| +| Production | `https://cloud.comfy.org` | + +## WebSocket Connection + +For real-time updates, connect to the WebSocket endpoint: + +``` +wss://cloud.comfy.org/ws?clientId={uuid}&token={api_key} +``` + +See the [API Reference](/development/cloud/api-reference#websocket-for-real-time-progress) for message types and handling. diff --git a/development/cloud/overview.mdx b/development/cloud/overview.mdx new file mode 100644 index 000000000..fa031a7c7 --- /dev/null +++ b/development/cloud/overview.mdx @@ -0,0 +1,177 @@ +--- +title: "Cloud API Overview" +description: "Programmatic access to ComfyUI Cloud for running workflows, managing files, and monitoring execution" +--- + + + **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. + + +# ComfyUI Cloud API + +The ComfyUI Cloud API provides programmatic access to run workflows on ComfyUI Cloud infrastructure. The API is compatible with local ComfyUI's API, making it easy to migrate existing integrations. + + + **Subscription Required:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + + +## 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 + + + + Please visit https://platform.comfy.org/login and log in with the corresponding account + ![Visit Platform Login Page](/images/interface/setting/user/user-login-api-key-1.jpg) + + + Click `+ New` in API Keys to create an API Key + ![Create API Key](/images/interface/setting/user/user-login-api-key-2.jpg) + + + + ![Enter API Key Name](/images/interface/setting/user/user-login-api-key-3.jpg) + 1. (Required) Enter the API Key name, + 2. Click `Generate` to create + + + ![Obtain API Key](/images/interface/setting/user/user-login-api-key-4.jpg) + + Since the API Key is only visible upon first creation, please save it immediately after creation. It cannot be viewed later, so please keep it safe. + Please note that you should not share your API Key with others. Once it leaked, you can delete it and create a new one. + + + + + + Keep your API key secure. Never commit it to version control or share it publicly. + + +### Using the API Key + +Pass your API key in the `X-API-Key` header with every request: + + +```bash curl +curl -X GET "https://cloud.comfy.org/api/user" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +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(); +``` + +```python Python +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 "Save (API Format)" 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 + +### Outputs + +Generated content (images, videos, audio) is stored in cloud storage. Output files can be downloaded via the `/api/view` endpoint or through signed URLs. + +## Quick Start + +Here's a minimal example to run a workflow: + + +```bash curl +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)"'}' +``` + +```typescript TypeScript +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(); +console.log(`Job submitted: ${result.prompt_id}`); +``` + +```python Python +import os +import requests +import json + +BASE_URL = "https://cloud.comfy.org" +API_KEY = os.environ["COMFY_CLOUD_API_KEY"] +headers = {"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=headers, + json={"prompt": workflow} +) +result = response.json() +prompt_id = result["prompt_id"] +print(f"Job submitted: {prompt_id}") +``` + + +## Available Endpoints + +| Category | Description | +|----------|-------------| +| [Workflows](/development/cloud/api-reference#running-workflows) | Submit workflows, check status | +| [Jobs](/development/cloud/api-reference#checking-job-status) | Monitor job status and queue | +| [Inputs](/development/cloud/api-reference#uploading-inputs) | Upload images, masks, and other inputs | +| [Outputs](/development/cloud/api-reference#downloading-outputs) | Download generated content | +| [WebSocket](/development/cloud/api-reference#websocket-for-real-time-progress) | Real-time progress updates | +| [Object Info](/development/cloud/api-reference#object-info) | Available nodes and their definitions | + +## Next Steps + +- [API Reference](/development/cloud/api-reference) - Complete endpoint documentation with examples +- [OpenAPI Specification](/development/cloud/openapi) - Machine-readable API spec diff --git a/development/overview.mdx b/development/overview.mdx index 640d2c26f..b04abf077 100644 --- a/development/overview.mdx +++ b/development/overview.mdx @@ -10,5 +10,6 @@ ComfyUI's key capabilities are: - **[Creating Workflows](/development/core-concepts/workflow)**: Workflows are a way to orchestrate AI models and automate tasks. They are a series of nodes that are connected together to form a pipeline. - **[Custom Nodes](/custom-nodes/overview)**: Custom nodes can be written by anyone to extend the capabilities of ComfyUI for your own use. Nodes are written in Python and are published by the community. - **Extensions**: Extensions are 3rd party applications that improve the UI of ComfyUI. -- **[Deployment](/development/comfyui-server/comms_overview)**: ComfyUI can be deployed in your own environment as an API endpoint. [Learn more] +- **[Local Server API](/development/comfyui-server/comms_overview)**: ComfyUI can be deployed in your own environment as an API endpoint. +- **[Cloud API](/development/cloud/overview)**: Run workflows programmatically on ComfyUI Cloud infrastructure without managing your own hardware. diff --git a/docs.json b/docs.json index 3c7e7d0e1..e71fcc753 100644 --- a/docs.json +++ b/docs.json @@ -540,6 +540,15 @@ "development/comfyui-server/api-key-integration" ] }, + { + "group": "Cloud API", + "icon": "cloud", + "pages": [ + "development/cloud/overview", + "development/cloud/api-reference", + "development/cloud/openapi" + ] + }, { "group": "CLI", "pages": [ @@ -677,6 +686,13 @@ { "tab": "Registry API Reference", "openapi": "https://api.comfy.org/openapi" + }, + { + "tab": "Cloud API Reference", + "openapi": { + "source": "openapi-cloud.yaml", + "directory": "api-reference/cloud" + } } ] }, diff --git a/get_started/cloud.mdx b/get_started/cloud.mdx index 1b5a303e7..37ec642ab 100644 --- a/get_started/cloud.mdx +++ b/get_started/cloud.mdx @@ -136,6 +136,10 @@ If you have any thoughts, suggestions, or run into any issues, simply click the ## Next steps + + Programmatically run workflows via the Cloud API + + Explore tutorials to learn ComfyUI workflows diff --git a/index.mdx b/index.mdx index c526dc496..1f68bc0c2 100644 --- a/index.mdx +++ b/index.mdx @@ -129,11 +129,18 @@ The most powerful open source node-based application for generative AI Create and publish custom nodes - Integrate ComfyUI with your applications + Integrate with local ComfyUI server + + + Run workflows via ComfyUI Cloud API diff --git a/openapi-cloud.yaml b/openapi-cloud.yaml new file mode 100644 index 000000000..e674f5e0a --- /dev/null +++ b/openapi-cloud.yaml @@ -0,0 +1,3699 @@ +openapi: 3.0.3 +info: + title: ComfyUI Cloud API + description: | + + **Experimental API:** This API is experimental and subject to change. + Endpoints, request/response formats, and behavior may be modified without notice. + + + API for ComfyUI Cloud - Run ComfyUI workflows on cloud infrastructure. + + This API allows you to interact with ComfyUI Cloud programmatically, including: + - Submitting and managing workflows + - Uploading and downloading files + - Monitoring job status and progress + + ## Cloud vs OSS ComfyUI Compatibility + + ComfyUI Cloud implements the same API interfaces as OSS ComfyUI for maximum compatibility, + but some fields are accepted for compatibility while being handled differently or ignored: + + | Field | Endpoints | Cloud Behavior | + |-------|-----------|----------------| + | `subfolder` | `/api/view`, `/api/upload/*` | **Ignored** - Cloud uses content-addressed storage (hash-based). Returned in responses for client-side organization. | + | `type` (input/output/temp) | `/api/view`, `/api/upload/*` | Partially used - All files stored with tag-based organization rather than directory structure. | + | `overwrite` | `/api/upload/*` | **Ignored** - Content-addressed storage means identical content always has the same hash. | + | `number`, `front` | `/api/prompt` | **Ignored** - Cloud uses its own fair queue scheduling per user. | + | `split`, `full_info` | `/api/userdata` | **Ignored** - Cloud always returns full file metadata. | + + These fields are retained in the API schema for drop-in compatibility with existing ComfyUI clients and workflows. + version: 1.0.0 + license: + name: GNU General Public License v3.0 + url: https://github.com/comfyanonymous/ComfyUI/blob/master/LICENSE + +servers: + - url: https://cloud.comfy.org + description: ComfyUI Cloud API + +# All endpoints require API key authentication unless marked with security: [] +security: + - ApiKeyAuth: [] + +tags: + - name: workflow + description: | + Submit workflows for execution and manage the execution queue. + This is the primary way to run ComfyUI workflows on the cloud. + - name: job + description: | + Monitor job status, view execution history, and manage running jobs. + Jobs are created when you submit a workflow via POST /api/prompt. + - name: asset + description: | + Upload, download, and manage persistent assets (images, models, outputs). + Assets provide durable storage with tagging and metadata support. + - name: file + description: | + Legacy file upload and download endpoints compatible with local ComfyUI. + For new integrations, consider using the Assets API instead. + - name: model + description: | + Browse available AI models. Models are pre-loaded on cloud infrastructure. + - name: node + description: | + Get information about available ComfyUI nodes and their inputs/outputs. + Useful for building dynamic workflow interfaces. + - name: user + description: | + User account information and personal data storage. + - name: system + description: | + Server status, health checks, and system information. + +x-tagGroups: + - name: Core + tags: + - workflow + - job + - name: Storage + tags: + - asset + - file + - name: Reference + tags: + - model + - node + - name: Account + tags: + - user + - system + +paths: + /api/prompt: + post: + tags: + - workflow + summary: Submit a workflow for execution + description: | + Submit a workflow to be executed by the backend. + The workflow is a JSON object describing the nodes and their connections. + operationId: executePrompt + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PromptRequest' + responses: + '200': + description: Success - Prompt accepted + content: + application/json: + schema: + $ref: '#/components/schemas/PromptResponse' + '400': + description: Invalid prompt + content: + application/json: + schema: + $ref: '#/components/schemas/PromptErrorResponse' + '402': + description: Payment required - Insufficient credits + content: + application/json: + schema: + $ref: '#/components/schemas/PromptErrorResponse' + '429': + description: Payment required - User has not paid + content: + application/json: + schema: + $ref: '#/components/schemas/PromptErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/PromptErrorResponse' + '503': + description: Service unavailable + content: + application/json: + schema: + $ref: '#/components/schemas/PromptErrorResponse' + get: + tags: + - workflow + summary: Get information about current prompt execution + description: Returns information about the current prompt in the execution queue + operationId: getPromptInfo + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/PromptInfo' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/object_info: + get: + tags: + - node + summary: Get all node information + description: Returns information about all available nodes + operationId: getNodeInfo + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/NodeInfo' + + /api/features: + get: + tags: + - node + summary: Get server feature flags + description: Returns the server's feature capabilities + operationId: getFeatures + security: + - ApiKeyAuth: [] + - {} # Also allows unauthenticated access (optional auth) + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + supports_preview_metadata: + type: boolean + description: Whether the server supports preview metadata + max_upload_size: + type: integer + description: Maximum upload size in bytes + additionalProperties: true + + /api/workflow_templates: + get: + tags: + - workflow + summary: Get available workflow templates + description: Returns available workflow templates + operationId: getWorkflowTemplates + security: [] # Public endpoint + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + description: Empty object for workflow templates + + /api/global_subgraphs: + get: + tags: + - workflow + summary: Get available subgraph blueprints + description: | + Returns a list of globally available subgraph blueprints. + These are pre-built workflow components that can be used as nodes. + The data field contains a promise that resolves to the full subgraph JSON. + operationId: getGlobalSubgraphs + security: [] # Public endpoint + responses: + '200': + description: Success - Map of subgraph IDs to their metadata + content: + application/json: + schema: + type: object + additionalProperties: + $ref: '#/components/schemas/GlobalSubgraphInfo' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/global_subgraphs/{id}: + get: + tags: + - workflow + summary: Get a specific subgraph blueprint + description: Returns the full data for a specific subgraph blueprint by ID + operationId: getGlobalSubgraph + security: [] # Public endpoint + parameters: + - name: id + in: path + required: true + description: The unique identifier of the subgraph blueprint + schema: + type: string + responses: + '200': + description: Success - Full subgraph data + content: + application/json: + schema: + $ref: '#/components/schemas/GlobalSubgraphData' + '404': + description: Subgraph not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/experiment/models: + get: + tags: + - model + summary: Get available model folders + description: | + Returns a list of model folders available in the system. + This is an experimental endpoint that replaces the legacy /models endpoint. + operationId: getModelFolders + security: [] # Public endpoint + responses: + '200': + description: Success - List of model folders + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ModelFolder' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/experiment/models/{folder}: + get: + tags: + - model + summary: Get models in a specific folder + description: | + Returns a list of models available in the specified folder. + This is an experimental endpoint that provides enhanced model information. + operationId: getModelsInFolder + security: [] # Public endpoint + parameters: + - name: folder + in: path + required: true + description: The folder name to list models from + schema: + type: string + example: "checkpoints" + responses: + '200': + description: Success - List of models in the folder + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ModelFile' + '404': + description: Folder not found or no models in folder + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/experiment/models/preview/{folder}/{path_index}/{filename}: + get: + tags: + - model + summary: Get model preview image + description: | + Returns a preview image for the specified model. + The image is returned in WebP format for optimal performance. + operationId: getModelPreview + security: [] # Public endpoint + parameters: + - name: folder + in: path + required: true + description: The folder name containing the model + schema: + type: string + example: "checkpoints" + - name: path_index + in: path + required: true + description: The path index (usually 0 for cloud service) + schema: + type: integer + example: 0 + - name: filename + in: path + required: true + description: The model filename (with or without .webp extension) + schema: + type: string + example: "model.safetensors" + responses: + '200': + description: Success - Model preview image + content: + image/webp: + schema: + type: string + format: binary + '404': + description: Model not found or preview not available + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/history: + post: + tags: + - job + summary: Manage execution history + description: | + Clear all history for the authenticated user or delete specific job IDs. + Supports clearing all history or deleting specific job IDs. + operationId: manageHistory + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/HistoryManageRequest' + responses: + '200': + description: Success - History management operation completed + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/history_v2: + get: + tags: + - job + summary: Get execution history (v2) + description: | + Retrieve execution history for the authenticated user with pagination support. + Returns a lightweight history format with filtered prompt data (workflow removed from extra_pnginfo). + operationId: getHistory + parameters: + - name: max_items + in: query + required: false + description: Maximum number of items to return + schema: + type: integer + - name: offset + in: query + required: false + description: Starting position (default 0) + schema: + type: integer + default: 0 + responses: + '200': + description: Success - Execution history retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/HistoryResponse' + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/history_v2/{prompt_id}: + get: + tags: + - job + summary: Get history for specific prompt + description: | + Retrieve detailed execution history for a specific prompt ID. + Returns full history data including complete prompt information. + operationId: getHistoryForPrompt + parameters: + - name: prompt_id + in: path + required: true + description: The prompt ID to retrieve history for + schema: + type: string + responses: + '200': + description: Success - History for prompt retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/HistoryDetailResponse' + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Prompt not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/jobs: + get: + tags: + - job + summary: List jobs with pagination and filtering + description: | + Retrieve a paginated list of jobs for the authenticated user. + Returns lightweight job data optimized for list views. + Workflow and full outputs are excluded to reduce payload size. + operationId: listJobs + parameters: + - name: status + in: query + required: false + description: Filter by one or more statuses (comma-separated). If not provided, returns all jobs. + schema: + type: string + example: pending,in_progress + - name: workflow_id + in: query + required: false + description: Filter by workflow ID (exact match) + schema: + type: string + example: 550e8400-e29b-41d4-a716-446655440000 + - name: output_type + in: query + required: false + description: Filter by output media type (only applies to completed jobs with outputs) + schema: + type: string + enum: [image, video, audio] + example: image + - name: sort_by + in: query + required: false + description: Field to sort by (create_time = when job was submitted, execution_time = how long workflow took to run) + schema: + type: string + enum: [create_time, execution_time] + default: create_time + example: execution_time + - name: sort_order + in: query + required: false + description: Sort direction (asc = ascending, desc = descending) + schema: + type: string + enum: [asc, desc] + default: desc + - name: offset + in: query + required: false + description: Pagination offset (0-based) + schema: + type: integer + minimum: 0 + default: 0 + - name: limit + in: query + required: false + description: Maximum items per page (1-1000) + schema: + type: integer + minimum: 1 + maximum: 1000 + default: 100 + responses: + '200': + description: Success - Jobs retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/JobsListResponse' + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/jobs/{job_id}: + get: + tags: + - job + summary: Get full job details + description: | + Retrieve complete details for a specific job including workflow and outputs. + Used for detail views, workflow re-execution, and debugging. + operationId: getJobDetail + parameters: + - name: job_id + in: path + required: true + description: Job identifier (UUID) + schema: + type: string + format: uuid + responses: + '200': + description: Success - Job details retrieved + content: + application/json: + schema: + $ref: '#/components/schemas/JobDetailResponse' + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden - Job does not belong to user + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Job not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/view: + get: + tags: + - file + summary: View a file + description: | + Retrieve and view a file from the ComfyUI file system. + This endpoint is typically used to view generated images or other output files. + operationId: viewFile + parameters: + - name: filename + in: query + required: true + description: Name of the file to view + schema: + type: string + example: "ComfyUI_00004_.png" + - name: subfolder + in: query + required: false + description: | + Subfolder path where the file is located. + **Note:** Accepted for ComfyUI API compatibility but **ignored** in cloud. + Cloud uses content-addressed storage where assets are stored by hash only. + The subfolder is client-side UI metadata and not used for storage lookup. + schema: + type: string + example: "tests/foo/bar" + - name: type + in: query + required: false + description: | + Type of file (e.g., output, input, temp). + **Note:** In cloud, both `output` and `temp` files are stored in the same bucket. + The type parameter is used for compatibility but storage location is determined by hash. + schema: + type: string + example: "output" + - name: fullpath + in: query + required: false + description: Full path to the file (used for temp files) + schema: + type: string + - name: format + in: query + required: false + description: Format of the file + schema: + type: string + - name: frame_rate + in: query + required: false + description: Frame rate for video files + schema: + type: integer + - name: workflow + in: query + required: false + description: Workflow identifier + schema: + type: string + - name: timestamp + in: query + required: false + description: Timestamp parameter + schema: + type: integer + example: "1234567890" + - name: channel + in: query + required: false + description: | + Image channel to extract from PNG images. + - 'rgb': Return only RGB channels (alpha set to fully opaque) + - 'a' or 'alpha': Return alpha channel as grayscale image + - If not specified, return original image unchanged via redirect + schema: + type: string + example: "rgb" + responses: + '302': + description: Redirect to GCS signed URL + headers: + Location: + description: Signed URL to access the file in GCS + schema: + type: string + '200': + description: Success - File content returned (used when channel parameter is present) + content: + image/png: + schema: + type: string + format: binary + description: Processed PNG image with extracted channel + '404': + description: File not found or unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/files/mask-layers: + get: + tags: + - file + summary: Get related mask layer files + description: | + Given a mask file (any of the 4 layers), returns all related mask layer files. + This is used by the mask editor to load the paint, mask, and painted layers + when reopening a previously edited mask. + operationId: getMaskLayers + parameters: + - name: filename + in: query + required: true + description: Hash filename of any mask layer file + schema: + type: string + example: "abc123def456.png" + responses: + '200': + description: Success - Related mask layers returned + content: + application/json: + schema: + type: object + properties: + mask: + type: string + description: Filename of the mask layer + nullable: true + paint: + type: string + description: Filename of the paint strokes layer + nullable: true + painted: + type: string + description: Filename of the painted image layer + nullable: true + painted_masked: + type: string + description: Filename of the final composite layer + nullable: true + '404': + description: File not found or not a mask file + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets: + get: + tags: + - asset + summary: List user assets + description: | + Retrieves a paginated list of assets belonging to the authenticated user. + Supports filtering by tags, name, metadata, and sorting options. + operationId: listAssets + parameters: + - name: include_tags + in: query + description: Filter assets that have ALL of these tags + schema: + type: array + items: + type: string + style: form + explode: false + - name: exclude_tags + in: query + description: Exclude assets that have ANY of these tags + schema: + type: array + items: + type: string + style: form + explode: false + - name: name_contains + in: query + description: Filter assets where name contains this substring (case-insensitive) + schema: + type: string + - name: metadata_filter + in: query + description: JSON object for filtering by metadata fields + schema: + type: string + - name: limit + in: query + description: Maximum number of assets to return (1-500) + schema: + type: integer + minimum: 1 + maximum: 500 + default: 20 + - name: offset + in: query + description: Number of assets to skip for pagination + schema: + type: integer + minimum: 0 + default: 0 + - name: sort + in: query + description: Field to sort by + schema: + type: string + enum: [name, created_at, updated_at, size, last_access_time] + default: created_at + - name: order + in: query + description: Sort order + schema: + type: string + enum: [asc, desc] + default: desc + - name: include_public + in: query + description: Whether to include public/shared assets in results + schema: + type: boolean + default: true + responses: + '200': + description: Success - Assets returned + content: + application/json: + schema: + $ref: '#/components/schemas/ListAssetsResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - asset + summary: Upload a new asset + description: | + Uploads a new asset to the system with associated metadata. + Supports two upload methods: + 1. Direct file upload (multipart/form-data) + 2. URL-based upload (application/json with source: "url") + + If an asset with the same hash already exists, returns the existing asset. + operationId: uploadAsset + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - file + properties: + file: + type: string + format: binary + description: The asset file to upload + tags: + type: array + items: + type: string + description: Freeform tags for the asset. Common types include "models", "input", "output", and "temp", but any tag can be used in any order. + id: + type: string + format: uuid + description: Optional asset ID for idempotent creation. If provided and asset exists, returns existing asset. + preview_id: + type: string + format: uuid + description: Optional preview asset ID. If not provided, images will use their own ID as preview. + name: + type: string + description: Display name for the asset + mime_type: + type: string + description: MIME type of the asset (e.g., "image/png", "video/mp4") + user_metadata: + type: string + description: Custom JSON metadata as a string + application/json: + schema: + type: object + required: + - url + - name + properties: + url: + type: string + format: uri + description: HTTP/HTTPS URL to download the asset from + name: + type: string + description: Display name for the asset (used to determine file extension) + tags: + type: array + items: + type: string + description: Freeform tags for the asset. Common types include "models", "input", "output", and "temp", but any tag can be used in any order. + user_metadata: + type: object + additionalProperties: true + description: Custom metadata to store with the asset + preview_id: + type: string + format: uuid + description: Optional preview asset ID + responses: + '201': + description: Asset created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AssetCreated' + '200': + description: Asset already exists (returned existing asset) + content: + application/json: + schema: + $ref: '#/components/schemas/AssetCreated' + '400': + description: Invalid request (bad file, invalid URL, invalid content type, etc.) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Source URL requires authentication or access denied + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Source URL not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '413': + description: File too large + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '415': + description: Unsupported media type + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: Download failed due to network error or timeout + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/from-hash: + post: + tags: + - asset + summary: Create asset reference from existing hash + description: | + Creates a new asset reference using an existing hash from cloud storage. + This avoids re-uploading file content when the underlying data already exists, + which is useful for large files or when referencing well-known assets. + The user provides their own metadata and tags for the new reference. + operationId: createAssetFromHash + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - hash + - tags + properties: + hash: + type: string + description: Hash of the existing asset. Supports Blake3 (blake3:) or SHA256 (sha256:) formats + pattern: '^(blake3|sha256):[a-f0-9]{64}$' + name: + type: string + description: Display name for the asset reference (optional) + tags: + type: array + items: + type: string + minItems: 1 + description: Freeform tags for the asset. Common types include "models", "input", "output", and "temp", but any tag can be used in any order. + mime_type: + type: string + description: MIME type of the asset (e.g., "image/png", "video/mp4") + user_metadata: + type: object + description: Custom metadata for this asset reference + additionalProperties: true + responses: + '201': + description: Asset reference created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AssetCreated' + '200': + description: Asset reference already exists (returned existing) + content: + application/json: + schema: + $ref: '#/components/schemas/AssetCreated' + '400': + description: Invalid request (bad hash format, invalid tags, etc.) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Source asset with given hash not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/remote-metadata: + get: + tags: + - asset + summary: Get asset metadata from remote URL + description: | + Retrieves metadata for an asset from a remote download URL without downloading the entire file. + Supports various sources including CivitAI and other model repositories. + Uses HEAD requests or API calls to fetch metadata efficiently. + This endpoint is for previewing metadata before downloading, not for getting metadata of existing assets. + operationId: getRemoteAssetMetadata + parameters: + - name: url + in: query + required: true + description: Download URL to retrieve metadata from + schema: + type: string + format: uri + example: 'https://civitai.com/api/download/models/123456' + responses: + '200': + description: Metadata retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AssetMetadataResponse' + '400': + description: Invalid URL or missing required parameter + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: Failed to retrieve metadata from source + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/download: + post: + tags: + - asset + summary: Initiate background download for large files + description: | + Initiates a background download job for large files from Huggingface or Civitai. + + If the file already exists in storage, the asset record is created immediately and returned (200 OK). + If the file doesn't exist, a background task is created and the task ID is returned (202 Accepted). + The frontend can track progress using GET /api/tasks/{task_id}. + operationId: createAssetDownload + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - source_url + properties: + source_url: + type: string + format: uri + description: URL of the file to download (must be from huggingface.co or civitai.com) + example: "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned.safetensors" + tags: + type: array + items: + type: string + description: Optional tags for the asset (e.g., ["model", "checkpoint"]) + user_metadata: + type: object + additionalProperties: true + description: Optional user-defined metadata to attach to the asset + preview_id: + type: string + format: uuid + description: Optional preview asset ID to associate with the downloaded asset + example: "550e8400-e29b-41d4-a716-446655440000" + responses: + '200': + description: File already exists in storage - asset created/returned immediately + content: + application/json: + schema: + $ref: '#/components/schemas/AssetCreated' + '202': + description: Accepted - Download task created and processing in background + content: + application/json: + schema: + $ref: '#/components/schemas/AssetDownloadResponse' + '400': + description: Invalid URL or unsupported source + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '422': + description: Validation errors + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/{id}: + get: + tags: + - asset + summary: Get asset details + description: Retrieves detailed information about a specific asset + operationId: getAssetById + parameters: + - name: id + in: path + required: true + description: Asset ID + schema: + type: string + format: uuid + responses: + '200': + description: Asset details retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/Asset' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + put: + tags: + - asset + summary: Update asset metadata + description: | + Updates an asset's metadata. At least one field must be provided. + Only name, tags, and user_metadata can be updated. + operationId: updateAsset + parameters: + - name: id + in: path + required: true + description: Asset ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: New display name for the asset + tags: + type: array + items: + type: string + description: Updated tags for the asset + mime_type: + type: string + description: Updated MIME type of the asset + preview_id: + type: string + format: uuid + description: Updated preview asset ID + user_metadata: + type: object + description: Updated custom metadata + additionalProperties: true + minProperties: 1 + responses: + '200': + description: Asset updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/AssetUpdated' + '400': + description: Invalid request (no fields provided) + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - asset + summary: Delete asset + description: | + Deletes an asset and its content from storage. + Both the database record and storage content are deleted. + operationId: deleteAsset + parameters: + - name: id + in: path + required: true + description: Asset ID + schema: + type: string + format: uuid + responses: + '204': + description: Asset deleted successfully + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/{id}/tags: + post: + tags: + - asset + summary: Add tags to asset + description: Adds one or more tags to an existing asset + operationId: addAssetTags + parameters: + - name: id + in: path + required: true + description: Asset ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - tags + properties: + tags: + type: array + items: + type: string + minItems: 1 + description: Tags to add to the asset + responses: + '200': + description: Tags added successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TagsModificationResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + delete: + tags: + - asset + summary: Remove tags from asset + description: Removes one or more tags from an existing asset + operationId: removeAssetTags + parameters: + - name: id + in: path + required: true + description: Asset ID + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - tags + properties: + tags: + type: array + items: + type: string + minItems: 1 + description: Tags to remove from the asset + responses: + '200': + description: Tags removed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TagsModificationResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/tags: + get: + tags: + - asset + summary: List all tags + description: | + Retrieves a list of all tags used across assets. + Includes usage counts and filtering options. + operationId: listTags + parameters: + - name: prefix + in: query + description: Filter tags by prefix + schema: + type: string + - name: limit + in: query + description: Maximum number of tags to return (1-1000) + schema: + type: integer + minimum: 1 + maximum: 1000 + default: 100 + - name: offset + in: query + description: Number of tags to skip for pagination + schema: + type: integer + minimum: 0 + default: 0 + - name: order + in: query + description: Sort order for tags + schema: + type: string + enum: [count_desc, name_asc] + default: count_desc + - name: include_zero + in: query + description: Include tags with zero usage count + schema: + type: boolean + default: false + - name: include_public + in: query + description: Whether to include public/shared assets when counting tags + schema: + type: boolean + default: true + responses: + '200': + description: Tags retrieved successfully + content: + application/json: + schema: + $ref: '#/components/schemas/ListTagsResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/tags/refine: + get: + tags: + - asset + summary: Get tag histogram for filtered assets + description: | + Returns a histogram of tags appearing on assets matching the given filters. + Useful for refining asset searches by showing available tags and their counts. + Only returns tags with non-zero counts (tags that exist on matching assets). + operationId: getAssetTagHistogram + parameters: + - name: include_tags + in: query + description: Filter assets that have ALL of these tags + schema: + type: array + items: + type: string + style: form + explode: false + - name: exclude_tags + in: query + description: Exclude assets that have ANY of these tags + schema: + type: array + items: + type: string + style: form + explode: false + - name: name_contains + in: query + description: Filter assets where name contains this substring (case-insensitive) + schema: + type: string + - name: metadata_filter + in: query + description: JSON object for filtering by metadata fields + schema: + type: string + - name: limit + in: query + description: Maximum number of tags to return (1-1000, default 100) + schema: + type: integer + minimum: 1 + maximum: 1000 + default: 100 + - name: include_public + in: query + description: Whether to include public/shared assets in results + schema: + type: boolean + default: true + responses: + '200': + description: Success - Tag histogram returned + content: + application/json: + schema: + $ref: '#/components/schemas/AssetTagHistogramResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/assets/hash/{hash}: + head: + tags: + - asset + summary: Check if asset exists by hash + description: | + Checks if content with the given hash exists in cloud storage. + Returns 200 if the content exists, 404 if it doesn't. + Useful for checking availability before using `/api/assets/from-hash`. + operationId: checkAssetByHash + parameters: + - name: hash + in: path + required: true + description: Blake3 hash of the asset in format 'blake3:hex_digest' + schema: + type: string + pattern: '^blake3:[a-f0-9]{64}$' + example: 'blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234' + responses: + '200': + description: Asset exists + '400': + description: Invalid hash format + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Asset not found + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + /api/queue: + get: + tags: + - job + summary: Get queue information + description: Returns information about running and pending items in the queue + operationId: getQueueInfo + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/QueueInfo' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + post: + tags: + - job + summary: Manage queue operations + description: | + Cancel specific PENDING jobs by ID or clear all pending jobs in the queue. + Note: This endpoint only affects pending jobs. To cancel running jobs, use /api/interrupt. + operationId: manageQueue + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/QueueManageRequest' + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/QueueManageResponse' + '400': + description: Invalid request parameters + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/interrupt: + post: + tags: + - job + summary: Interrupt currently running jobs + description: | + Cancel all currently RUNNING jobs for the authenticated user. + This will interrupt any job that is currently in 'in_progress' status. + Note: This endpoint only affects running jobs. To cancel pending jobs, use /api/queue. + operationId: interruptJob + responses: + '200': + description: Success - Job interrupted or no running job found + '401': + description: Unauthorized - Authentication required + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/userdata: + get: + tags: + - user + operationId: getUserdata + summary: List user data files + description: Returns a list of user data files in the specified directory, optionally recursively and with full metadata. + parameters: + - name: dir + in: query + required: true + description: | + The directory path to list files from. Must include trailing slash. + Example: "workflows/" or "settings/" + schema: + type: string + example: "workflows/" + - name: recurse + in: query + required: false + description: If true, include files in subdirectories. Otherwise only lists files directly in the specified directory. + schema: + type: boolean + default: false + - name: split + in: query + required: false + description: | + Whether to split file information by type. + **Note:** Accepted for ComfyUI API compatibility but currently ignored. + schema: + type: boolean + default: false + - name: full_info + in: query + required: false + description: | + Whether to return full file metadata. + **Note:** Accepted for ComfyUI API compatibility but currently ignored (always returns full info). + schema: + type: boolean + default: false + responses: + '200': + description: A list of user data files. + content: + application/json: + schema: + $ref: '#/components/schemas/GetUserDataResponseFull' + '400': + description: Bad request (e.g., invalid filename). + content: + text/plain: + schema: + type: string + '401': + description: Unauthorized. + content: + text/plain: + schema: + type: string + '404': + description: File not found or invalid path. + content: + text/plain: + schema: + type: string + '500': + description: General error + content: + text/plain: + schema: + type: string + + /api/userdata/{file}: + get: + tags: + - user + operationId: getUserdataFile + summary: Get user data file + description: Returns the requested user data file if it exists. + parameters: + - name: file + in: path + required: true + description: The filename of the user data to retrieve. + schema: + type: string + responses: + '200': + description: Successfully retrieved the file. + content: + application/octet-stream: + schema: + type: string + format: binary + '400': + description: Bad request (e.g., invalid filename). + content: + text/plain: + schema: + type: string + '401': + description: Unauthorized. + content: + text/plain: + schema: + type: string + '404': + description: File not found or invalid path. + content: + text/plain: + schema: + type: string + '500': + description: General error + content: + text/plain: + schema: + type: string + post: + tags: + - user + operationId: postUserdataFile + summary: Upload or update a user data file + description: | + Upload a file to a user's data directory. Optional query parameters allow + control over overwrite behavior and response detail. + parameters: + - name: file + in: path + required: true + description: The target file path (URL encoded if necessary). + schema: + type: string + - name: overwrite + in: query + required: false + description: If "false", prevents overwriting existing files. Defaults to "true". + schema: + type: string + enum: ["true", "false"] + default: "true" + - name: full_info + in: query + required: false + description: If "true", returns detailed file info; if "false", returns only the relative path. + schema: + type: string + enum: ["true", "false"] + default: "false" + requestBody: + required: true + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: File uploaded successfully. + content: + application/json: + schema: + $ref: '#/components/schemas/UserDataResponseFull' + '400': + description: Missing or invalid 'file' parameter. + content: + text/plain: + schema: + type: string + '401': + description: Unauthorized. + content: + text/plain: + schema: + type: string + '403': + description: The requested path is not allowed. + content: + text/plain: + schema: + type: string + '409': + description: File already exists and overwrite is set to false. + content: + text/plain: + schema: + type: string + '500': + description: General error + content: + text/plain: + schema: + type: string + delete: + tags: + - user + operationId: deleteUserdataFile + summary: Delete a user data file + description: | + Delete a user data file from the database. The file parameter should be + the relative path within the user's data directory. + parameters: + - name: file + in: path + required: true + description: The file path to delete (URL encoded if necessary). + schema: + type: string + responses: + '204': + description: File deleted successfully (No Content). + '401': + description: Unauthorized. + content: + text/plain: + schema: + type: string + '404': + description: File not found. + content: + text/plain: + schema: + type: string + '500': + description: Internal server error. + content: + text/plain: + schema: + type: string + + /api/upload/image: + post: + tags: + - file + summary: Upload an image file + description: Upload an image file to cloud storage + operationId: uploadImage + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - image + properties: + image: + type: string + format: binary + description: The image file to upload + overwrite: + type: string + description: | + Whether to overwrite existing file (true/false). + **Note:** Accepted for ComfyUI API compatibility but effectively ignored in cloud. + Cloud uses content-addressed storage (hash-based deduplication), so identical + content always maps to the same hash. Re-uploading the same content is a no-op. + subfolder: + type: string + description: | + Optional subfolder path. + **Note:** Accepted for ComfyUI API compatibility but **ignored** for storage. + Cloud stores assets by hash only. The subfolder is returned in the response + for client-side organization but is not used for server-side storage paths. + type: + type: string + description: | + Upload type (defaults to "output"). + **Note:** Accepted for ComfyUI API compatibility. Cloud stores all uploads + as assets with tags rather than directory-based organization. + responses: + '200': + description: Image uploaded successfully + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Filename of the uploaded image + subfolder: + type: string + description: Subfolder path where image was saved + type: + type: string + description: Type of upload (e.g., "output") + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/upload/mask: + post: + tags: + - file + summary: Upload a mask image + description: Upload a mask image to be applied to an existing image + operationId: uploadMask + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + required: + - image + - original_ref + properties: + image: + type: string + format: binary + description: The mask image file to upload + original_ref: + type: string + description: JSON string containing reference to the original image + responses: + '200': + description: Mask uploaded successfully + content: + application/json: + schema: + type: object + properties: + name: + type: string + description: Filename of the uploaded mask + subfolder: + type: string + description: Subfolder path where mask was saved + type: + type: string + description: Type of upload (e.g., "output") + metadata: + type: object + description: Additional metadata for mask detection and re-editing + properties: + is_mask: + type: boolean + description: Whether this file is a mask + original_hash: + type: string + description: Hash of the original unmasked image + mask_type: + type: string + description: Type of mask (e.g., "painted_masked") + related_files: + type: object + description: Related mask layer files (if available) + properties: + mask: + type: string + description: Hash of the mask layer + paint: + type: string + description: Hash of the paint layer + painted: + type: string + description: Hash of the painted image + '400': + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/system_stats: + get: + tags: + - system + summary: Get system statistics + description: Returns system statistics including ComfyUI version, device info, and system resources + operationId: getSystemStats + security: [] # Public endpoint + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/SystemStatsResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/user: + get: + tags: + - user + summary: Get current user information + description: Returns information about the currently authenticated user + operationId: getUser + responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/UserResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + + /api/job/{job_id}/status: + get: + tags: + - job + summary: Get job status + description: Returns the current status of a specific job by ID + operationId: getJobStatus + parameters: + - name: job_id + in: path + required: true + description: The unique ID of the job + schema: + type: string + format: uuid + responses: + '200': + description: Success - Job status returned + content: + application/json: + schema: + $ref: '#/components/schemas/JobStatusResponse' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '403': + description: Forbidden - job belongs to another user + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '404': + description: Job not found + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + +components: + securitySchemes: + ApiKeyAuth: + type: apiKey + in: header + name: X-API-Key + description: | + API key authentication. Generate an API key from your account settings + at https://comfy.org/account. Pass the key in the X-API-Key header. + schemas: + PromptRequest: + type: object + required: + - prompt + properties: + prompt: + type: object + description: The workflow graph to execute + additionalProperties: true + number: + type: number + description: | + Priority number for the queue (lower numbers have higher priority). + **Note:** Accepted for ComfyUI API compatibility but **ignored** in cloud. + Cloud uses its own queue management with per-user ordering and fair scheduling. + front: + type: boolean + description: | + If true, adds the prompt to the front of the queue. + **Note:** Accepted for ComfyUI API compatibility but **ignored** in cloud. + Cloud manages queue ordering internally based on job submission time and fair scheduling. + extra_data: + type: object + description: Extra data to be associated with the prompt + additionalProperties: true + partial_execution_targets: + type: array + items: + type: string + description: List of node names to execute + + PromptResponse: + type: object + properties: + prompt_id: + type: string + format: uuid + description: Unique identifier for the prompt execution + number: + type: number + description: Priority number in the queue + node_errors: + type: object + description: Any errors in the nodes of the prompt + additionalProperties: true + + ErrorResponse: + type: object + required: + - code + - message + properties: + code: + type: string + message: + type: string + + PromptInfo: + type: object + properties: + exec_info: + type: object + properties: + queue_remaining: + type: integer + description: Number of items remaining in the queue + + NodeInfo: + type: object + properties: + input: + type: object + description: Input specifications for the node + additionalProperties: true + input_order: + type: object + description: Order of inputs for display + additionalProperties: + type: array + items: + type: string + output: + type: array + items: + type: string + description: Output types of the node + output_is_list: + type: array + items: + type: boolean + description: Whether each output is a list + output_name: + type: array + items: + type: string + description: Names of the outputs + name: + type: string + description: Internal name of the node + display_name: + type: string + description: Display name of the node + description: + type: string + description: Description of the node + python_module: + type: string + description: Python module implementing the node + category: + type: string + description: Category of the node + output_node: + type: boolean + description: Whether this is an output node + output_tooltips: + type: array + items: + type: string + description: Tooltips for outputs + deprecated: + type: boolean + description: Whether the node is deprecated + experimental: + type: boolean + description: Whether the node is experimental + api_node: + type: boolean + description: Whether this is an API node + + GlobalSubgraphInfo: + type: object + description: Metadata for a global subgraph blueprint (without full data) + required: + - source + - name + - info + properties: + source: + type: string + description: Source type of the subgraph - "templates" for workflow templates or "custom_node" for custom node subgraphs + name: + type: string + description: Display name of the subgraph blueprint + info: + type: object + description: Additional information about the subgraph + required: + - node_pack + properties: + node_pack: + type: string + description: The node pack/module that provides this subgraph + data: + type: string + description: The full subgraph JSON data (may be empty in list view) + + GlobalSubgraphData: + type: object + description: Full data for a global subgraph blueprint + required: + - source + - name + - info + - data + properties: + source: + type: string + description: Source type of the subgraph - "templates" for workflow templates or "custom_node" for custom node subgraphs + name: + type: string + description: Display name of the subgraph blueprint + info: + type: object + description: Additional information about the subgraph + required: + - node_pack + properties: + node_pack: + type: string + description: The node pack/module that provides this subgraph + data: + type: string + description: The full subgraph JSON data as a string + + HistoryResponse: + type: object + description: | + Execution history response with history array. + Returns an object with a "history" key containing an array of history entries. + Each entry includes prompt_id as a property along with execution data. + required: + - history + properties: + history: + type: array + description: Array of history entries ordered by creation time (newest first) + items: + $ref: '#/components/schemas/HistoryEntry' + + HistoryEntry: + type: object + description: History entry with prompt_id and execution data + required: + - prompt_id + properties: + prompt_id: + type: string + description: Unique identifier for this prompt execution + create_time: + type: integer + format: int64 + description: Job creation timestamp (Unix timestamp in milliseconds) + workflow_id: + type: string + description: UUID identifying the workflow graph definition + prompt: + type: object + description: Filtered prompt execution data (lightweight format) + properties: + priority: + type: number + format: double + description: Execution priority + prompt_id: + type: string + description: The prompt ID + extra_data: + type: object + description: Additional execution data (workflow removed from extra_pnginfo) + additionalProperties: true + outputs: + type: object + description: Output data from execution (generated images, files, etc.) + additionalProperties: true + status: + type: object + description: Execution status and timeline information + additionalProperties: true + meta: + type: object + description: Metadata about the execution and nodes + additionalProperties: true + + HistoryDetailEntry: + type: object + description: History entry with full prompt data + properties: + prompt: + type: object + description: Full prompt execution data + properties: + priority: + type: number + format: double + description: Execution priority + prompt_id: + type: string + description: The prompt ID + prompt: + type: object + description: The workflow nodes + additionalProperties: true + extra_data: + type: object + description: Additional execution data + additionalProperties: true + outputs_to_execute: + type: array + items: + type: string + description: Output nodes to execute + outputs: + type: object + description: Output data from execution (generated images, files, etc.) + additionalProperties: true + status: + type: object + description: Execution status and timeline information + additionalProperties: true + meta: + type: object + description: Metadata about the execution and nodes + additionalProperties: true + + HistoryDetailResponse: + type: object + description: | + Detailed execution history response for a specific prompt. + Returns a dictionary with prompt_id as key and full history data as value. + additionalProperties: + $ref: '#/components/schemas/HistoryDetailEntry' + + QueueInfo: + type: object + description: Queue information with pending and running jobs + properties: + queue_running: + type: array + description: Array of currently running job items + items: + type: array + description: | + Queue item tuple format: [job_number, prompt_id, workflow_json, output_node_ids, metadata] + - [0] job_number (integer): Position in queue (1-based) + - [1] prompt_id (string): Job UUID + - [2] workflow_json (object): Full ComfyUI workflow + - [3] output_node_ids (array): Node IDs to return results from + - [4] metadata (object): Contains {create_time: } + queue_pending: + type: array + description: Array of pending job items (ordered by creation time, oldest first) + items: + type: array + description: | + Queue item tuple format: [job_number, prompt_id, workflow_json, output_node_ids, metadata] + - [0] job_number (integer): Position in queue (1-based) + - [1] prompt_id (string): Job UUID + - [2] workflow_json (object): Full ComfyUI workflow + - [3] output_node_ids (array): Node IDs to return results from + - [4] metadata (object): Contains {create_time: } + + QueueManageRequest: + type: object + description: Request to manage queue operations + properties: + delete: + type: array + items: + type: string + description: Array of PENDING job IDs to cancel + clear: + type: boolean + description: If true, clear all pending jobs from the queue + additionalProperties: false + + QueueManageResponse: + type: object + properties: + deleted: + type: array + items: + type: string + description: Array of job IDs that were successfully cancelled + cleared: + type: boolean + description: Whether the queue was cleared + + JobStatusResponse: + type: object + description: Job status information + properties: + id: + type: string + format: uuid + description: The job ID + status: + type: string + enum: [waiting_to_dispatch, pending, in_progress, completed, error, cancelled] + description: Current job status + created_at: + type: string + format: date-time + description: When the job was created + updated_at: + type: string + format: date-time + description: When the job was last updated + last_state_update: + type: string + format: date-time + description: When the job status was last changed + assigned_inference: + type: string + nullable: true + description: The inference instance assigned to this job (if any) + error_message: + type: string + nullable: true + description: Error message if the job failed + required: + - id + - status + - created_at + - updated_at + + HistoryManageRequest: + type: object + description: Request to manage history operations + properties: + delete: + type: array + items: + type: string + description: Array of job IDs to delete from history + clear: + type: boolean + description: If true, clear all history for the authenticated user + additionalProperties: false + UserDataResponse: + oneOf: + - $ref: '#/components/schemas/UserDataResponseFull' + - $ref: '#/components/schemas/UserDataResponseShort' + UserDataResponseFull: + type: object + properties: + path: + type: string + size: + type: integer + modified: + type: string + format: date-time + UserDataResponseShort: + type: string + GetUserDataResponseFull: + type: array + items: + $ref: '#/components/schemas/GetUserDataResponseFullFile' + + GetUserDataResponseFullFile: + type: object + properties: + path: + type: string + description: File name or path relative to the user directory. + size: + type: integer + description: File size in bytes. + modified: + type: number + format: float + description: UNIX timestamp of the last modification. + PromptErrorResponse: + type: object + description: Error response for ComfyUI prompt execution. + additionalProperties: true + + ModelFolder: + type: object + description: Represents a folder containing models + required: + - name + - folders + properties: + name: + type: string + description: The name of the model folder + example: "checkpoints" + folders: + type: array + items: + type: string + description: List of paths where models of this type are stored + example: ["checkpoints"] + + ModelFile: + type: object + description: Represents a model file with metadata + required: + - name + - pathIndex + properties: + name: + type: string + description: The filename of the model + example: "model.safetensors" + pathIndex: + type: integer + description: Index of the path where this model is located + example: 0 + + SystemStatsResponse: + type: object + description: System statistics response + required: + - system + - devices + properties: + system: + type: object + required: + - os + - python_version + - embedded_python + - comfyui_version + - pytorch_version + - argv + - ram_total + - ram_free + properties: + os: + type: string + description: Operating system + python_version: + type: string + description: Python version + embedded_python: + type: boolean + description: Whether using embedded Python + comfyui_version: + type: string + description: ComfyUI version + comfyui_frontend_version: + type: string + description: ComfyUI frontend version (commit hash or tag) + workflow_templates_version: + type: string + description: Workflow templates version + cloud_version: + type: string + description: Cloud ingest service version (commit hash) + pytorch_version: + type: string + description: PyTorch version + argv: + type: array + items: + type: string + description: Command line arguments + ram_total: + type: number + description: Total RAM in bytes + ram_free: + type: number + description: Free RAM in bytes + devices: + type: array + items: + type: object + required: + - name + - type + properties: + name: + type: string + description: Device name + type: + type: string + description: Device type + vram_total: + type: number + description: Total VRAM in bytes + vram_free: + type: number + description: Free VRAM in bytes + + UserResponse: + type: object + description: User information response + required: + - status + properties: + status: + type: string + description: User status (active or waitlisted) + + Asset: + type: object + required: + - id + - name + - size + - created_at + - updated_at + properties: + id: + type: string + format: uuid + description: Unique identifier for the asset + name: + type: string + description: Name of the asset file + asset_hash: + type: string + description: Blake3 hash of the asset content + pattern: '^blake3:[a-f0-9]{64}$' + size: + type: integer + format: int64 + description: Size of the asset in bytes + mime_type: + type: string + description: MIME type of the asset + tags: + type: array + items: + type: string + description: Tags associated with the asset + user_metadata: + type: object + description: Custom user metadata for the asset + additionalProperties: true + preview_url: + type: string + format: uri + description: URL for asset preview/thumbnail + preview_id: + type: string + format: uuid + description: ID of the preview asset if available + nullable: true + prompt_id: + type: string + format: uuid + description: ID of the job/prompt that created this asset, if available + nullable: true + created_at: + type: string + format: date-time + description: Timestamp when the asset was created + updated_at: + type: string + format: date-time + description: Timestamp when the asset was last updated + last_access_time: + type: string + format: date-time + description: Timestamp when the asset was last accessed + is_immutable: + type: boolean + description: Whether this asset is immutable (cannot be modified or deleted) + + AssetCreated: + allOf: + - $ref: '#/components/schemas/Asset' + - type: object + required: + - created_new + properties: + created_new: + type: boolean + description: Whether this was a new asset creation (true) or returned existing (false) + + AssetUpdated: + type: object + required: + - id + - updated_at + properties: + id: + type: string + format: uuid + description: Asset ID + name: + type: string + description: Updated name of the asset + asset_hash: + type: string + description: Blake3 hash of the asset content + pattern: '^blake3:[a-f0-9]{64}$' + tags: + type: array + items: + type: string + description: Updated tags for the asset + mime_type: + type: string + description: Updated MIME type of the asset + user_metadata: + type: object + description: Updated custom metadata + additionalProperties: true + updated_at: + type: string + format: date-time + description: Timestamp of the update + + ListAssetsResponse: + type: object + required: + - assets + - total + - has_more + properties: + assets: + type: array + items: + $ref: '#/components/schemas/Asset' + description: List of assets matching the query + total: + type: integer + description: Total number of assets matching the filters + has_more: + type: boolean + description: Whether more assets are available beyond this page + + TagInfo: + type: object + required: + - name + - count + properties: + name: + type: string + description: Tag name + count: + type: integer + description: Number of assets using this tag + + ListTagsResponse: + type: object + required: + - tags + - total + - has_more + properties: + tags: + type: array + items: + $ref: '#/components/schemas/TagInfo' + description: List of tags + total: + type: integer + description: Total number of tags + has_more: + type: boolean + description: Whether more tags are available + + AssetTagHistogramResponse: + type: object + required: + - tag_counts + properties: + tag_counts: + type: object + additionalProperties: + type: integer + description: Map of tag names to their occurrence counts on matching assets + example: + checkpoint: 32 + lora: 193 + vae: 6 + + AssetMetadataResponse: + type: object + required: + - content_length + properties: + content_length: + type: integer + format: int64 + description: Size of the asset in bytes (-1 if unknown) + example: 4294967296 + content_type: + type: string + description: MIME type of the asset + example: "application/octet-stream" + filename: + type: string + description: Suggested filename for the asset from source + example: "realistic-vision-v5.safetensors" + name: + type: string + description: Display name or title for the asset from source + example: "Realistic Vision v5.0" + tags: + type: array + items: + type: string + description: Tags for categorization from source + example: ["models", "checkpoint"] + preview_image: + type: string + description: Preview image as base64-encoded data URL + example: "data:image/jpeg;base64,/9j/4AAQSkZJRg..." + validation: + $ref: '#/components/schemas/ValidationResult' + description: Validation results for the file + + AssetDownloadResponse: + type: object + required: + - task_id + - status + properties: + task_id: + type: string + format: uuid + description: Task ID for tracking download progress via GET /api/tasks/{task_id} + status: + type: string + enum: [created, running, completed, failed] + description: Current task status + message: + type: string + description: Human-readable message + example: "Download task created. Use task_id to track progress." + + ValidationResult: + type: object + required: + - is_valid + properties: + is_valid: + type: boolean + description: Overall validation status (true if all checks passed) + example: true + errors: + type: array + items: + $ref: '#/components/schemas/ValidationError' + description: Blocking validation errors that prevent download + warnings: + type: array + items: + $ref: '#/components/schemas/ValidationError' + description: Non-blocking validation warnings (informational only) + + ValidationError: + type: object + required: + - code + - message + - field + properties: + code: + type: string + description: Machine-readable error code + example: FORMAT_NOT_ALLOWED + message: + type: string + description: Human-readable error message + example: "File format \"PickleTensor\" is not allowed. Allowed formats: [SafeTensor]" + field: + type: string + description: Field that failed validation + example: format + + TagsModificationResponse: + type: object + required: + - total_tags + properties: + added: + type: array + items: + type: string + description: Tags that were successfully added (for add operation) + removed: + type: array + items: + type: string + description: Tags that were successfully removed (for remove operation) + already_present: + type: array + items: + type: string + description: Tags that were already present (for add operation) + not_present: + type: array + items: + type: string + description: Tags that were not present (for remove operation) + total_tags: + type: array + items: + type: string + description: All tags on the asset after the operation + + # Jobs API Schemas + JobsListResponse: + type: object + required: + - jobs + - pagination + properties: + jobs: + type: array + description: Array of jobs ordered by specified sort field + items: + $ref: '#/components/schemas/JobEntry' + pagination: + $ref: '#/components/schemas/PaginationInfo' + + JobEntry: + type: object + description: Lightweight job data for list views (workflow and full outputs excluded) + required: + - id + - status + - create_time + properties: + id: + type: string + format: uuid + description: Unique job identifier + status: + type: string + enum: [pending, in_progress, completed, failed, cancelled] + description: User-friendly job status + execution_error: + $ref: '#/components/schemas/ExecutionError' + description: Detailed execution error from ComfyUI (only for failed jobs with structured error data) + create_time: + type: integer + format: int64 + description: Job creation timestamp (Unix timestamp in seconds) + preview_output: + type: object + description: Primary preview output (only present for terminal states) + additionalProperties: true + outputs_count: + type: integer + description: Total number of output files (omitted for non-terminal states) + workflow_id: + type: string + description: UUID identifying the workflow graph definition + execution_start_time: + type: integer + format: int64 + description: Workflow execution start timestamp (Unix milliseconds, only present for terminal states) + execution_end_time: + type: integer + format: int64 + description: Workflow execution completion timestamp (Unix milliseconds, only present for terminal states) + + JobDetailResponse: + type: object + description: Full job details including workflow and outputs + required: + - id + - status + - create_time + - update_time + properties: + id: + type: string + format: uuid + description: Unique job identifier + status: + type: string + enum: [pending, in_progress, completed, failed, cancelled] + description: User-friendly job status + workflow: + type: object + description: Full ComfyUI workflow (10-100KB, omitted if not available) + additionalProperties: true + execution_error: + $ref: '#/components/schemas/ExecutionError' + description: Detailed execution error from ComfyUI (only for failed jobs with structured error data) + create_time: + type: integer + format: int64 + description: Job creation timestamp (Unix timestamp in seconds) + update_time: + type: integer + format: int64 + description: Last update timestamp (Unix timestamp in seconds) + outputs: + type: object + description: Full outputs object from ComfyUI (only for terminal states) + additionalProperties: true + preview_output: + type: object + description: Primary preview output (only for terminal states) + additionalProperties: true + outputs_count: + type: integer + description: Total number of output files (omitted for non-terminal states) + workflow_id: + type: string + description: UUID identifying the workflow graph definition + execution_status: + type: object + description: ComfyUI execution status and timeline (only for terminal states) + additionalProperties: true + execution_meta: + type: object + description: Node-level execution metadata (only for terminal states) + additionalProperties: true + + ExecutionError: + type: object + description: Detailed execution error information from ComfyUI + required: + - node_id + - node_type + - exception_message + - exception_type + - traceback + - current_inputs + - current_outputs + properties: + node_id: + type: string + description: ID of the node that failed + node_type: + type: string + description: Type name of the node (e.g., "KSampler") + exception_message: + type: string + description: Human-readable error message + exception_type: + type: string + description: Python exception type (e.g., "RuntimeError") + traceback: + type: array + items: + type: string + description: Array of traceback lines (empty array if not available) + current_inputs: + type: object + additionalProperties: true + description: Input values at time of failure (empty object if not available) + current_outputs: + type: object + additionalProperties: true + description: Output values at time of failure (empty object if not available) + + PaginationInfo: + type: object + required: + - offset + - limit + - total + - has_more + properties: + offset: + type: integer + minimum: 0 + description: Current offset (0-based) + limit: + type: integer + minimum: 1 + description: Items per page + total: + type: integer + minimum: 0 + description: Total number of items matching filters + has_more: + type: boolean + description: Whether more items are available beyond this page + + # ========================================================================= + # WEBSOCKET MESSAGE SCHEMAS + # ========================================================================= + # WebSocket messages are sent over the /ws endpoint for real-time updates + # during workflow execution. Connect with: /ws?clientId={uuid}&token={auth_token} + + WebSocketMessage: + type: object + description: | + Base structure for all WebSocket messages. Messages are JSON with a type + field indicating the message type and a data field containing type-specific payload. + required: + - type + properties: + type: + $ref: '#/components/schemas/WebSocketMessageType' + data: + type: object + description: Message payload (structure varies by type) + additionalProperties: true + + WebSocketMessageType: + type: string + description: | + Type of WebSocket message. These match ComfyUI's native message types. + enum: + - status + - execution_start + - execution_cached + - executing + - progress + - progress_state + - executed + - execution_success + - execution_error + - execution_interrupted + + WebSocketStatusMessage: + type: object + description: | + Queue status update. Sent when the queue state changes. + properties: + type: + type: string + enum: [status] + data: + type: object + properties: + status: + type: object + properties: + exec_info: + type: object + properties: + queue_remaining: + type: integer + description: Number of jobs remaining in queue for this user + sid: + type: string + description: Session ID (only on initial connection) + + WebSocketExecutionStartMessage: + type: object + description: | + Sent when workflow execution begins. + properties: + type: + type: string + enum: [execution_start] + data: + type: object + required: + - prompt_id + properties: + prompt_id: + type: string + format: uuid + description: The job/prompt ID that started executing + + WebSocketExecutingMessage: + type: object + description: | + Sent when a specific node starts executing. + properties: + type: + type: string + enum: [executing] + data: + type: object + required: + - prompt_id + properties: + prompt_id: + type: string + format: uuid + node: + type: string + description: The node ID currently executing (null when execution completes) + display_node: + type: string + description: The display node ID (may differ from node for grouped nodes) + + WebSocketProgressMessage: + type: object + description: | + Step-by-step progress within a node (e.g., diffusion sampling steps). + properties: + type: + type: string + enum: [progress] + data: + type: object + required: + - prompt_id + - node + - value + - max + properties: + prompt_id: + type: string + format: uuid + node: + type: string + description: Node ID showing progress + value: + type: integer + description: Current step number + max: + type: integer + description: Total number of steps + + WebSocketProgressStateMessage: + type: object + description: | + Extended progress state with detailed node metadata. + Provides richer context than the simpler `progress` message. + properties: + type: + type: string + enum: [progress_state] + data: + type: object + properties: + prompt_id: + type: string + format: uuid + nodes: + type: object + description: Map of node IDs to their progress state + additionalProperties: + type: object + properties: + node_id: + type: string + display_node_id: + type: string + real_node_id: + type: string + prompt_id: + type: string + class_type: + type: string + description: The node's class type (e.g., "KSampler") + + WebSocketExecutedMessage: + type: object + description: | + Sent when a node completes execution with outputs. + properties: + type: + type: string + enum: [executed] + data: + type: object + required: + - prompt_id + - node + properties: + prompt_id: + type: string + format: uuid + node: + type: string + description: Node ID that completed + display_node: + type: string + description: Display node ID + output: + type: object + description: | + Node outputs. Structure varies by node type. + For image outputs: { "images": [{"filename": "...", "subfolder": "", "type": "output"}] } + For video outputs: { "video": [{"filename": "...", "subfolder": "", "type": "output"}] } + additionalProperties: true + + WebSocketExecutionSuccessMessage: + type: object + description: | + Sent when the entire workflow completes successfully. + properties: + type: + type: string + enum: [execution_success] + data: + type: object + required: + - prompt_id + properties: + prompt_id: + type: string + format: uuid + + WebSocketExecutionErrorMessage: + type: object + description: | + Sent when workflow execution fails with an error. + properties: + type: + type: string + enum: [execution_error] + data: + type: object + required: + - prompt_id + - exception_message + properties: + prompt_id: + type: string + format: uuid + node_id: + type: string + description: Node ID where error occurred (if applicable) + node_type: + type: string + description: Type of node where error occurred + exception_type: + type: string + description: | + Error classification. Common types: + - ValidationError: Invalid workflow or inputs + - ModelDownloadError: Failed to download required model + - OOMError: Out of GPU memory + - InsufficientFundsError: Account balance too low + - InactiveSubscriptionError: Subscription not active + enum: + - ValidationError + - ModelDownloadError + - ImageDownloadError + - OOMError + - PanicError + - ServiceError + - WebSocketError + - DispatcherError + - InsufficientFundsError + - InactiveSubscriptionError + exception_message: + type: string + description: Human-readable error message + traceback: + type: array + items: + type: string + description: Stack trace lines (for debugging) + executed: + type: array + items: + type: string + description: Node IDs that completed before the error + current_inputs: + type: object + description: Input values at time of failure + additionalProperties: true + current_outputs: + type: object + description: Output values at time of failure + additionalProperties: true + + WebSocketExecutionCachedMessage: + type: object + description: | + Sent when nodes are skipped because their outputs are cached. + properties: + type: + type: string + enum: [execution_cached] + data: + type: object + required: + - prompt_id + - nodes + properties: + prompt_id: + type: string + format: uuid + nodes: + type: array + items: + type: string + description: List of node IDs that were served from cache + + WebSocketExecutionInterruptedMessage: + type: object + description: | + Sent when workflow execution is cancelled/interrupted. + properties: + type: + type: string + enum: [execution_interrupted] + data: + type: object + required: + - prompt_id + properties: + prompt_id: + type: string + format: uuid + + # Binary WebSocket Message Types (sent as binary frames, not JSON) + # All multi-byte integers are big-endian. + # + # Type 1: PREVIEW_IMAGE + # Format: [type:4B][image_type:4B][image_data:...] + # - type: 0x00000001 (big-endian uint32) + # - image_type: format code (big-endian uint32) + # - image_data: raw JPEG or PNG bytes + # + # Type 3: TEXT + # Format: [type:4B][node_id_len:4B][node_id:...][text:...] + # - type: 0x00000003 (big-endian uint32) + # - node_id_len: length of node_id string (big-endian uint32) + # - node_id: UTF-8 encoded node ID + # - text: UTF-8 encoded progress text + # + # Type 4: PREVIEW_IMAGE_WITH_METADATA + # Format: [type:4B][metadata_len:4B][metadata_json:...][image_data:...] + # - type: 0x00000004 (big-endian uint32) + # - metadata_len: length of metadata JSON (big-endian uint32) + # - metadata_json: UTF-8 JSON with fields: + # - node_id: string + # - display_node_id: string + # - real_node_id: string + # - prompt_id: string (uuid) + # - parent_node_id: string | null + # - image_data: raw JPEG or PNG bytes From 0d9dbfcef31eef0c1d015b25b0d1d087d8391508 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:31:32 -0500 Subject: [PATCH 02/27] Add Chinese translations for Cloud API documentation - Create zh-CN/development/cloud/overview.mdx - Create zh-CN/development/cloud/api-reference.mdx - Create zh-CN/development/cloud/openapi.mdx - Update zh-CN/development/overview.mdx with Cloud API link - Update zh-CN/get_started/cloud.mdx with Cloud API card - Update zh-CN/index.mdx with Cloud API card in development section - Fix branding: use 'Comfy Cloud' consistently (not 'ComfyUI Cloud') --- development/cloud/api-reference.mdx | 6 +- development/cloud/openapi.mdx | 8 +- development/cloud/overview.mdx | 8 +- zh-CN/development/cloud/api-reference.mdx | 1314 +++++++++++++++++++++ zh-CN/development/cloud/openapi.mdx | 72 ++ zh-CN/development/cloud/overview.mdx | 177 +++ zh-CN/development/overview.mdx | 5 +- zh-CN/get_started/cloud.mdx | 8 +- zh-CN/index.mdx | 11 +- 9 files changed, 1592 insertions(+), 17 deletions(-) create mode 100644 zh-CN/development/cloud/api-reference.mdx create mode 100644 zh-CN/development/cloud/openapi.mdx create mode 100644 zh-CN/development/cloud/overview.mdx diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index b54c406e5..407e93b25 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -1,6 +1,6 @@ --- title: "Cloud API Reference" -description: "Complete API reference with code examples for ComfyUI Cloud" +description: "Complete API reference with code examples for Comfy Cloud" --- @@ -9,10 +9,10 @@ description: "Complete API reference with code examples for ComfyUI Cloud" # Cloud API Reference -This page provides complete examples for common ComfyUI Cloud API operations. +This page provides complete examples for common Comfy Cloud API operations. - **Subscription Required:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. ## Setup diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx index b8e608d27..01e0ba853 100644 --- a/development/cloud/openapi.mdx +++ b/development/cloud/openapi.mdx @@ -1,6 +1,6 @@ --- title: "OpenAPI Specification" -description: "Machine-readable OpenAPI specification for ComfyUI Cloud API" +description: "Machine-readable OpenAPI specification for Comfy Cloud API" openapi: "/openapi-cloud.yaml" --- @@ -8,12 +8,12 @@ openapi: "/openapi-cloud.yaml" **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. -# ComfyUI Cloud API Specification +# Comfy Cloud API Specification -This page provides the complete OpenAPI 3.0 specification for the ComfyUI Cloud API. +This page provides the complete OpenAPI 3.0 specification for the Comfy Cloud API. - **Subscription Required:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. ## Interactive API Reference diff --git a/development/cloud/overview.mdx b/development/cloud/overview.mdx index fa031a7c7..39df8d456 100644 --- a/development/cloud/overview.mdx +++ b/development/cloud/overview.mdx @@ -1,18 +1,18 @@ --- title: "Cloud API Overview" -description: "Programmatic access to ComfyUI Cloud for running workflows, managing files, and monitoring execution" +description: "Programmatic access to Comfy Cloud for running workflows, managing files, and monitoring execution" --- **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. -# ComfyUI Cloud API +# Comfy Cloud API -The ComfyUI Cloud API provides programmatic access to run workflows on ComfyUI Cloud infrastructure. The API is compatible with local ComfyUI's API, making it easy to migrate existing integrations. +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:** Running workflows via the API requires an active ComfyUI Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. ## Base URL diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx new file mode 100644 index 000000000..51f430160 --- /dev/null +++ b/zh-CN/development/cloud/api-reference.mdx @@ -0,0 +1,1314 @@ +--- +title: "Cloud API 参考" +description: "Comfy Cloud 的完整 API 参考及代码示例" +--- + + + **实验性 API:** 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。 + + +# Cloud API 参考 + +本页面提供了常见 Comfy Cloud API 操作的完整示例。 + + + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。请查看[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)了解详情。 + + +## 设置 + +所有示例都使用以下通用的导入和配置: + + +```bash curl +export COMFY_CLOUD_API_KEY="your-api-key" +export BASE_URL="https://cloud.comfy.org" +``` + +```typescript TypeScript +import { readFile, writeFile } from "fs/promises"; + +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", + }; +} +``` + +```python Python +import os +import requests +import json +import time +import asyncio +import aiohttp + +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" + } +``` + + +--- + +## 对象信息 + +获取可用的节点定义。这对于了解可用的节点及其输入/输出规范非常有用。 + + +```bash curl +curl -X GET "$BASE_URL/api/object_info" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function getObjectInfo(): Promise> { + const response = await fetch(`${BASE_URL}/api/object_info`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const objectInfo = await getObjectInfo(); +console.log(`Available nodes: ${Object.keys(objectInfo).length}`); + +const ksampler = objectInfo["KSampler"] ?? {}; +console.log(`KSampler inputs: ${Object.keys(ksampler.input?.required ?? {})}`); +``` + +```python Python +def get_object_info(): + """Fetch all available node definitions from cloud.""" + response = requests.get( + f"{BASE_URL}/api/object_info", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +# Get all nodes +object_info = get_object_info() +print(f"Available nodes: {len(object_info)}") + +# Get a specific node's definition +ksampler = object_info.get("KSampler", {}) +print(f"KSampler inputs: {list(ksampler.get('input', {}).get('required', {}).keys())}") +``` + + +--- + +## 上传输入 + +上传图像、遮罩或其他文件以在工作流中使用。 + +### 直接上传(Multipart) + + +```bash curl +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" +``` + +```typescript TypeScript +async function uploadInput( + filePath: string, + inputType: string = "input" +): Promise<{ name: string; subfolder: string }> { + const file = await readFile(filePath); + const formData = new FormData(); + formData.append("image", new Blob([file]), filePath.split("/").pop()); + formData.append("type", inputType); + formData.append("overwrite", "true"); + + const response = await fetch(`${BASE_URL}/api/upload/image`, { + method: "POST", + headers: { "X-API-Key": API_KEY }, + body: formData, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const result = await uploadInput("my_image.png"); +console.log(`Uploaded: ${result.name} to ${result.subfolder}`); +``` + +```python Python +def upload_input(file_path: str, input_type: str = "input") -> dict: + """Upload a file directly to cloud. + + Args: + file_path: Path to the file to upload + input_type: "input" for images, "temp" for temporary files + + Returns: + Upload response with filename and subfolder + """ + with open(file_path, "rb") as f: + files = {"image": f} + data = {"type": input_type, "overwrite": "true"} + + response = requests.post( + f"{BASE_URL}/api/upload/image", + headers={"X-API-Key": API_KEY}, # No Content-Type for multipart + files=files, + data=data + ) + response.raise_for_status() + return response.json() + +# Upload an image +result = upload_input("my_image.png") +print(f"Uploaded: {result['name']} to {result['subfolder']}") +``` + + +### 上传遮罩 + + +```bash curl +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"}' +``` + +```typescript TypeScript +async function uploadMask( + filePath: string, + originalRef: { filename: string; subfolder: string; type: string } +): Promise<{ name: string; subfolder: string }> { + const file = await readFile(filePath); + const formData = new FormData(); + formData.append("image", new Blob([file]), filePath.split("/").pop()); + formData.append("type", "input"); + formData.append("subfolder", "clipspace"); + formData.append("original_ref", JSON.stringify(originalRef)); + + const response = await fetch(`${BASE_URL}/api/upload/mask`, { + method: "POST", + headers: { "X-API-Key": API_KEY }, + body: formData, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const maskResult = await uploadMask("mask.png", { + filename: "my_image.png", + subfolder: "", + type: "input", +}); +console.log(`Uploaded mask: ${maskResult.name}`); +``` + +```python Python +def upload_mask(file_path: str, original_ref: dict) -> dict: + """Upload a mask associated with an original image. + + Args: + file_path: Path to the mask file + original_ref: Reference to the original image {"filename": "...", "subfolder": "...", "type": "..."} + """ + with open(file_path, "rb") as f: + files = {"image": f} + data = { + "type": "input", + "subfolder": "clipspace", + "original_ref": json.dumps(original_ref) + } + + response = requests.post( + f"{BASE_URL}/api/upload/mask", + headers={"X-API-Key": API_KEY}, + files=files, + data=data + ) + response.raise_for_status() + return response.json() +``` + + +### 引用已知资源(跳过上传) + +如果您知道某个文件已存在于云存储中(例如,Comfy 共享的热门示例图像),您可以创建资源引用而无需上传任何字节。首先检查哈希是否存在,然后创建您自己的引用。 + + +```bash curl +# 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" + }' +``` + +```typescript TypeScript +async function checkHashExists(hash: string): Promise { + // Note: /api/assets/hash/{hash} only accepts blake3: format + const response = await fetch(`${BASE_URL}/api/assets/hash/${hash}`, { + method: "HEAD", + }); + return response.ok; +} + +async function createAssetFromHash( + hash: string, + name: string, + tags: string[] = ["input"] +): Promise<{ id: string; hash: string }> { + // Note: /api/assets/from-hash accepts both blake3: and sha256: formats + const response = await fetch(`${BASE_URL}/api/assets/from-hash`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ hash, name, tags }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +// Use a well-known sample image without uploading +// Hash format: blake3:<64 hex chars> +const sampleHash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"; + +if (await checkHashExists(sampleHash)) { + const asset = await createAssetFromHash(sampleHash, "sample_portrait.png"); + console.log(`Created asset ${asset.id} pointing to ${asset.hash}`); + + // Use in workflow (assuming workflow is already loaded) + // workflow["1"]["inputs"]["image"] = asset.hash; +} +``` + +```python Python +def check_hash_exists(hash: str) -> bool: + """Check if a file hash exists in cloud storage (unauthenticated). + + Note: This endpoint only accepts blake3: format hashes. + """ + response = requests.head(f"{BASE_URL}/api/assets/hash/{hash}") + return response.status_code == 200 + +def create_asset_from_hash(hash: str, name: str, tags: list = None) -> dict: + """Create an asset reference from an existing hash. + + This skips uploading bytes entirely - useful for well-known files + or files you've previously uploaded to the cloud. + + Note: This endpoint accepts both blake3: and sha256: format hashes. + """ + response = requests.post( + f"{BASE_URL}/api/assets/from-hash", + headers=get_headers(), + json={ + "hash": hash, + "name": name, + "tags": tags or ["input"] + } + ) + response.raise_for_status() + return response.json() + +# Use a well-known sample image without uploading +# Hash format: blake3:<64 hex chars> or sha256:<64 hex chars> +sample_hash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" + +if check_hash_exists(sample_hash): + asset = create_asset_from_hash(sample_hash, "sample_portrait.png") + print(f"Created asset {asset['id']} pointing to {asset['hash']}") + + # Use in workflow + workflow["1"]["inputs"]["image"] = asset["hash"] +``` + + +--- + +## 运行工作流 + +提交工作流以执行。 + +### 提交工作流 + + +```bash curl +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)"'}' +``` + +```typescript TypeScript +async function submitWorkflow(workflow: Record): Promise { + 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}`); + const result = await response.json(); + + if (result.error) { + throw new Error(`Workflow error: ${result.error}`); + } + return result.prompt_id; +} + +const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8")); +const promptId = await submitWorkflow(workflow); +console.log(`Submitted job: ${promptId}`); +``` + +```python Python +def submit_workflow(workflow: dict) -> str: + """Submit a workflow and return the prompt_id (job ID). + + Args: + workflow: ComfyUI workflow in API format + + Returns: + prompt_id for tracking the job + """ + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={"prompt": workflow} + ) + response.raise_for_status() + result = response.json() + + if "error" in result: + raise ValueError(f"Workflow error: {result['error']}") + + return result["prompt_id"] + +# Load and submit a workflow +with open("workflow_api.json") as f: + workflow = json.load(f) + +prompt_id = submit_workflow(workflow) +print(f"Submitted job: {prompt_id}") +``` + + +### 修改工作流输入 + + +```typescript TypeScript +function setWorkflowInput( + workflow: Record, + nodeId: string, + inputName: string, + value: any +): Record { + 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"); +``` + +```python Python +def set_workflow_input(workflow: dict, node_id: str, input_name: str, value) -> dict: + """Modify a workflow input value. + + Args: + workflow: The workflow dict + node_id: ID of the node to modify + input_name: Name of the input field + value: New value + + Returns: + Modified workflow + """ + if node_id in workflow: + workflow[node_id]["inputs"][input_name] = value + return workflow + +# Example: Set seed and prompt +workflow = set_workflow_input(workflow, "3", "seed", 12345) +workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") +``` + + +--- + +## 检查任务状态 + +轮询任务完成状态。 + + +```bash curl +# Get job status +curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +interface JobStatus { + status: string; + outputs?: Record; +} + +async function getJobStatus(promptId: string): Promise { + 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 { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout * 1000) { + const status = await getJobStatus(promptId); + + if (status.status === "completed") { + return status; + } else if (["error", "failed", "cancelled"].includes(status.status)) { + throw new Error(`Job failed with status: ${status.status}`); + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + throw new Error(`Job ${promptId} did not complete within ${timeout}s`); +} + +const result = await pollForCompletion(promptId); +console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); +``` + +```python Python +def get_job_status(prompt_id: str) -> dict: + """Get the status of a job. + + Returns: + Job status with fields: status, outputs (if complete) + """ + response = requests.get( + f"{BASE_URL}/api/job/{prompt_id}/status", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> dict: + """Poll until job completes or times out. + + Args: + prompt_id: The job ID + timeout: Maximum seconds to wait + poll_interval: Seconds between polls + + Returns: + Final job status + """ + start_time = time.time() + + while time.time() - start_time < timeout: + status = get_job_status(prompt_id) + job_status = status.get("status", "unknown") + + if job_status == "completed": + return status + elif job_status in ("error", "failed", "cancelled"): + raise RuntimeError(f"Job failed with status: {job_status}") + + # Still pending or in_progress + time.sleep(poll_interval) + + raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s") + +# Wait for job to complete +result = poll_for_completion(prompt_id) +print(f"Job completed! Outputs: {result.get('outputs', {}).keys()}") +``` + + +--- + +## 实时进度 WebSocket + +连接 WebSocket 以获取实时执行更新。 + + +```typescript TypeScript +async function listenForCompletion( + promptId: string, + timeout: number = 300000 +): Promise> { + const wsUrl = `wss://cloud.comfy.org/ws?clientId=${crypto.randomUUID()}&token=${API_KEY}`; + const outputs: Record = {}; + + 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); +``` + +```python Python +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. + + Args: + prompt_id: The job ID to monitor + timeout: Maximum seconds to wait + + Returns: + Final outputs from the job + """ + # Build WebSocket URL + ws_url = BASE_URL.replace("https://", "wss://").replace("http://", "ws://") + 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}") + else: + print("Execution complete") + + 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") + node_type = msg_data.get("node_type", "") + raise RuntimeError(f"Execution error in {node_type}: {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") + + return outputs + +# Usage +async def run_with_websocket(): + prompt_id = submit_workflow(workflow) + outputs = await listen_for_completion(prompt_id) + return outputs + +# Run async +outputs = asyncio.run(run_with_websocket()) +``` + + +### 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_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 结构:** + ```json + { + "node_id": "3", + "display_node_id": "3", + "real_node_id": "3", + "prompt_id": "abc-123", + "parent_node_id": null + } + ``` + + + + + 请参阅 [OpenAPI 规范](/zh-CN/development/cloud/openapi) 了解每种 JSON 消息类型的完整模式定义。 + + +--- + +## 下载输出 + +在任务完成后检索生成的文件。 + + +```bash curl +# 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 +``` + +```typescript TypeScript +async function downloadOutput( + filename: string, + subfolder: string = "", + outputType: string = "output" +): Promise { + const params = new URLSearchParams({ filename, subfolder, type: outputType }); + const response = await fetch(`${BASE_URL}/api/view?${params}`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.arrayBuffer(); +} + +async function saveOutputs( + outputs: Record, + outputDir: string = "." +): Promise { + 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}`); + } + } + } +} + +await saveOutputs(outputs, "./my_outputs"); +``` + +```python Python +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(): + # Handle image outputs + if "images" in node_outputs: + for img_info in node_outputs["images"]: + filename = img_info["filename"] + subfolder = img_info.get("subfolder", "") + output_type = img_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}") + + # Handle video outputs + if "video" in node_outputs: + for vid_info in node_outputs["video"]: + filename = vid_info["filename"] + subfolder = vid_info.get("subfolder", "") + output_type = vid_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}") + +# After job completes +save_outputs(outputs, "./my_outputs") +``` + + +--- + +## 完整端到端示例 + +以下是一个将所有内容整合在一起的完整示例: + + +```typescript TypeScript +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): Promise { + 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> { + const wsUrl = `wss://cloud.comfy.org/ws?clientId=${crypto.randomUUID()}&token=${API_KEY}`; + const outputs: Record = {}; + + 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, + outputDir: string +): Promise { + 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(); +``` + +```python Python +import os +import requests +import json +import asyncio +import aiohttp +import uuid + +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"} + +def upload_image(file_path: str) -> dict: + """Upload an image and return the reference for use in workflows.""" + with open(file_path, "rb") as f: + response = requests.post( + f"{BASE_URL}/api/upload/image", + headers={"X-API-Key": API_KEY}, + files={"image": f}, + data={"type": "input", "overwrite": "true"} + ) + response.raise_for_status() + return response.json() + +def submit_workflow(workflow: dict) -> str: + """Submit workflow and return prompt_id.""" + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={"prompt": workflow} + ) + response.raise_for_status() + return response.json()["prompt_id"] + +async def wait_for_completion(prompt_id: str, timeout: float = 300.0) -> dict: + """Wait for job completion via WebSocket.""" + ws_url = BASE_URL.replace("https://", "wss://") + f"/ws?clientId={uuid.uuid4()}&token={API_KEY}" + outputs = {} + + async with aiohttp.ClientSession() as session: + async with session.ws_connect(ws_url) as ws: + start = asyncio.get_event_loop().time() + async for msg in ws: + if asyncio.get_event_loop().time() - start > timeout: + raise TimeoutError("Job timed out") + + if msg.type != aiohttp.WSMsgType.TEXT: + continue + + data = json.loads(msg.data) + if data.get("data", {}).get("prompt_id") != prompt_id: + continue + + msg_type = data.get("type") + msg_data = data.get("data", {}) + + if msg_type == "progress": + print(f"Progress: {msg_data.get('value')}/{msg_data.get('max')}") + elif msg_type == "executed": + if output := msg_data.get("output"): + outputs[msg_data["node"]] = output + elif msg_type == "execution_success": + return outputs + elif msg_type == "execution_error": + raise RuntimeError(msg_data.get("exception_message", "Unknown error")) + + return outputs + +def download_outputs(outputs: dict, output_dir: str): + """Download all output files.""" + os.makedirs(output_dir, exist_ok=True) + + for node_outputs in outputs.values(): + for key in ["images", "video", "audio"]: + for file_info in node_outputs.get(key, []): + params = { + "filename": file_info["filename"], + "subfolder": file_info.get("subfolder", ""), + "type": file_info.get("type", "output") + } + response = requests.get(f"{BASE_URL}/api/view", headers=get_headers(), params=params) + response.raise_for_status() + + path = os.path.join(output_dir, file_info["filename"]) + with open(path, "wb") as f: + f.write(response.content) + print(f"Downloaded: {path}") + +async def main(): + # 1. Load workflow + with open("workflow_api.json") as f: + workflow = json.load(f) + + # 2. Optionally upload input images + # image_ref = upload_image("input.png") + # workflow["1"]["inputs"]["image"] = image_ref["name"] + + # 3. Modify workflow parameters + workflow["3"]["inputs"]["seed"] = 42 + workflow["6"]["inputs"]["text"] = "a beautiful sunset over mountains" + + # 4. Submit workflow + prompt_id = submit_workflow(workflow) + print(f"Job submitted: {prompt_id}") + + # 5. Wait for completion with progress + outputs = await wait_for_completion(prompt_id) + print(f"Job completed! Found {len(outputs)} output nodes") + + # 6. Download outputs + download_outputs(outputs, "./outputs") + print("Done!") + +if __name__ == "__main__": + asyncio.run(main()) +``` + + +--- + +## 队列管理 + +### 获取队列状态 + + +```bash curl +curl -X GET "$BASE_URL/api/queue" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function getQueue(): Promise<{ + queue_running: any[]; + queue_pending: any[]; +}> { + const response = await fetch(`${BASE_URL}/api/queue`, { + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return response.json(); +} + +const queue = await getQueue(); +console.log(`Running: ${queue.queue_running.length}`); +console.log(`Pending: ${queue.queue_pending.length}`); +``` + +```python Python +def get_queue(): + """Get current queue status.""" + response = requests.get( + f"{BASE_URL}/api/queue", + headers=get_headers() + ) + response.raise_for_status() + return response.json() + +queue = get_queue() +print(f"Running: {len(queue.get('queue_running', []))}") +print(f"Pending: {len(queue.get('queue_pending', []))}") +``` + + +### 取消任务 + + +```bash curl +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"]}' +``` + +```typescript TypeScript +async function cancelJob(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/queue`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ delete: [promptId] }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); +} +``` + +```python Python +def cancel_job(prompt_id: str): + """Cancel a pending or running job.""" + response = requests.post( + f"{BASE_URL}/api/queue", + headers=get_headers(), + json={"delete": [prompt_id]} + ) + response.raise_for_status() + return response.json() +``` + + +### 中断当前执行 + + +```bash curl +curl -X POST "$BASE_URL/api/interrupt" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +async function interrupt(): Promise { + const response = await fetch(`${BASE_URL}/api/interrupt`, { + method: "POST", + headers: getHeaders(), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); +} +``` + +```python Python +def interrupt(): + """Interrupt the currently running job.""" + response = requests.post( + f"{BASE_URL}/api/interrupt", + headers=get_headers() + ) + response.raise_for_status() +``` + + +--- + +## 错误处理 + +API 通过 `execution_error` WebSocket 消息或 HTTP 错误响应返回结构化错误。`exception_type` 字段标识错误类别: + +| 异常类型 | 描述 | +|----------|------| +| `ValidationError` | 无效的工作流或输入 | +| `ModelDownloadError` | 所需模型不可用或下载失败 | +| `ImageDownloadError` | 从 URL 下载输入图像失败 | +| `OOMError` | GPU 内存不足 | +| `PanicError` | 执行期间发生意外服务器崩溃 | +| `ServiceError` | 内部服务错误 | +| `WebSocketError` | WebSocket 连接或通信错误 | +| `DispatcherError` | 任务分发或路由错误 | +| `InsufficientFundsError` | 账户余额不足 | +| `InactiveSubscriptionError` | 订阅未激活 | + + +```typescript TypeScript +async function handleApiError(response: Response): Promise { + 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); +} +``` + +```python Python +def handle_api_error(response): + """Handle API error responses.""" + if response.status_code == 402: + raise ValueError("Insufficient credits. Please add funds to your account.") + elif response.status_code == 429: + raise ValueError("Rate limited or subscription inactive.") + elif response.status_code >= 400: + try: + error = response.json() + raise ValueError(f"API error: {error.get('message', response.text)}") + except json.JSONDecodeError: + raise ValueError(f"API error: {response.text}") +``` + diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx new file mode 100644 index 000000000..c99b24f40 --- /dev/null +++ b/zh-CN/development/cloud/openapi.mdx @@ -0,0 +1,72 @@ +--- +title: "OpenAPI 规范" +description: "Comfy Cloud API 的机器可读的 OpenAPI 规范" +openapi: "/openapi-cloud.yaml" +--- + + + **实验性 API:** 此 API 为实验性质,可能会发生变化。端点、请求/响应格式和行为可能会在不另行通知的情况下进行修改。 + + +# Comfy Cloud API 规范 + +本页面提供 Comfy Cloud API 的完整 OpenAPI 3.0 规范。 + + + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 + + +## 交互式 API 参考 + +通过交互式文档探索完整的 API: + + + 浏览端点、查看模式并尝试请求 + + +## 使用规范 + +OpenAPI 规范可用于: + +- **生成客户端库** - 使用 [OpenAPI Generator](https://openapi-generator.tech/) 等工具生成任何语言的客户端库 +- **导入 API 工具** - 如 Postman、Insomnia 或 Paw +- **生成文档** - 使用 Redoc 或 Swagger UI 等工具 +- **验证请求** - 在您的应用程序中验证请求 + +## 下载 + +您可以下载原始 OpenAPI 规范文件: + + + 下载 OpenAPI 3.0 规范 + + +## 认证 + +所有 API 请求都需要在 `X-API-Key` 请求头中传递 API 密钥。 + +### 获取 API 密钥 + +请参阅 [Cloud API 概述](/zh-CN/development/cloud/overview#getting-an-api-key)了解带有截图的分步说明。 + +### 使用 API 密钥 + +```yaml +X-API-Key: your-api-key +``` + +## 基础 URL + +| 环境 | URL | +|------|-----| +| 生产环境 | `https://cloud.comfy.org` | + +## WebSocket 连接 + +如需实时更新,请连接到 WebSocket 端点: + +``` +wss://cloud.comfy.org/ws?clientId={uuid}&token={api_key} +``` + +请参阅 [API 参考](/zh-CN/development/cloud/api-reference#websocket-for-real-time-progress)了解消息类型和处理方法。 diff --git a/zh-CN/development/cloud/overview.mdx b/zh-CN/development/cloud/overview.mdx new file mode 100644 index 000000000..d26ec7ce3 --- /dev/null +++ b/zh-CN/development/cloud/overview.mdx @@ -0,0 +1,177 @@ +--- +title: "Cloud API 概述" +description: "通过编程方式访问 Comfy Cloud,在云端运行工作流、管理文件并监控执行状态" +--- + + + **实验性 API:** 此 API 目前处于实验阶段,可能会发生变更。端点、请求/响应格式和行为可能会在不另行通知的情况下进行修改。 + + +# Comfy Cloud API + +Comfy Cloud API 提供以编程方式访问 Comfy Cloud 的能力,可在云端基础设施上运行工作流。该 API 与本地 ComfyUI 的 API 兼容,便于迁移现有集成。 + + + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 + + +## 基础 URL + +``` +https://cloud.comfy.org +``` + +## 身份验证 + +所有 API 请求都需要通过 `X-API-Key` 请求头传递 API 密钥。 + +### 获取 API 密钥 + + + + 请访问 https://platform.comfy.org/login 并使用相应账户登录 + ![访问平台登录页面](/images/interface/setting/user/user-login-api-key-1.jpg) + + + 点击 API Keys 中的 `+ New` 创建 API Key + ![创建 API Key](/images/interface/setting/user/user-login-api-key-2.jpg) + + + + ![输入 API Key 名称](/images/interface/setting/user/user-login-api-key-3.jpg) + 1. (必填)输入 API Key 名称, + 2. 点击 `Generate` 生成 + + + ![获取 API Key](/images/interface/setting/user/user-login-api-key-4.jpg) + + 由于 API Key 仅在首次创建时可见,请在创建后立即保存。之后将无法再次查看,请妥善保管。 + 请注意不要与他人分享您的 API Key。一旦泄露,您可以删除它并创建新的。 + + + + + + 请妥善保管您的 API 密钥。切勿将其提交到版本控制系统或公开分享。 + + +### 使用 API 密钥 + +在每个请求中通过 `X-API-Key` 请求头传递您的 API 密钥: + + +```bash curl +curl -X GET "https://cloud.comfy.org/api/user" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +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(); +``` + +```python Python +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 +) +``` + + +## 核心概念 + +### 工作流 + +ComfyUI 工作流是描述节点图的 JSON 对象。API 接受"API 格式"的工作流(以节点 ID 为键,包含 class_type、inputs 等),该格式由 ComfyUI 前端的"Save (API Format)"选项导出。 + +### 任务 + +当您提交工作流时,会创建一个**任务**。任务以异步方式执行: +1. 通过 `POST /api/prompt` 提交工作流 +2. 收到 `prompt_id`(任务 ID) +3. 通过 WebSocket 监控进度或轮询状态 +4. 完成后获取输出 + +### 输出 + +生成的内容(图像、视频、音频)存储在云存储中。输出文件可通过 `/api/view` 端点或签名 URL 下载。 + +## 快速入门 + +以下是运行工作流的最小示例: + + +```bash curl +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)"'}' +``` + +```typescript TypeScript +const BASE_URL = "https://cloud.comfy.org"; +const API_KEY = process.env.COMFY_CLOUD_API_KEY!; + +// 加载您的工作流(从 ComfyUI 以 API 格式导出) +const workflow = JSON.parse(await Deno.readTextFile("workflow_api.json")); + +// 提交工作流 +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(); +console.log(`Job submitted: ${result.prompt_id}`); +``` + +```python Python +import os +import requests +import json + +BASE_URL = "https://cloud.comfy.org" +API_KEY = os.environ["COMFY_CLOUD_API_KEY"] +headers = {"X-API-Key": API_KEY, "Content-Type": "application/json"} + +# 加载您的工作流(从 ComfyUI 以 API 格式导出) +with open("workflow_api.json") as f: + workflow = json.load(f) + +# 提交工作流 +response = requests.post( + f"{BASE_URL}/api/prompt", + headers=headers, + json={"prompt": workflow} +) +result = response.json() +prompt_id = result["prompt_id"] +print(f"Job submitted: {prompt_id}") +``` + + +## 可用端点 + +| 类别 | 描述 | +|----------|-------------| +| [工作流](/zh-CN/development/cloud/api-reference#running-workflows) | 提交工作流、检查状态 | +| [任务](/zh-CN/development/cloud/api-reference#checking-job-status) | 监控任务状态和队列 | +| [输入](/zh-CN/development/cloud/api-reference#uploading-inputs) | 上传图像、遮罩和其他输入 | +| [输出](/zh-CN/development/cloud/api-reference#downloading-outputs) | 下载生成的内容 | +| [WebSocket](/zh-CN/development/cloud/api-reference#websocket-for-real-time-progress) | 实时进度更新 | +| [对象信息](/zh-CN/development/cloud/api-reference#object-info) | 可用节点及其定义 | + +## 后续步骤 + +- [API 参考](/zh-CN/development/cloud/api-reference) - 完整的端点文档和示例 +- [OpenAPI 规范](/zh-CN/development/cloud/openapi) - 机器可读的 API 规范 diff --git a/zh-CN/development/overview.mdx b/zh-CN/development/overview.mdx index d7b354afd..fbda95851 100644 --- a/zh-CN/development/overview.mdx +++ b/zh-CN/development/overview.mdx @@ -8,6 +8,7 @@ ComfyUI 是一个强大的 GenAI 推理引擎,可用于本地运行 AI 模型 ComfyUI 的主要功能包括: - **[创建工作流](/zh-CN/development/core-concepts/workflow)**:工作流是一种编排 AI 模型和自动化任务的方式。它们是一系列相互连接形成管道的节点。 -- **[自定义节点](/zh-CN/development/core-concepts/custom-nodes)**:任何人都可以编写自定义节点来扩展 ComfyUI 的功能。节点使用 Python 编写,并由社区发布。 +- **[自定义节点](/zh-CN/custom-nodes/overview)**:任何人都可以编写自定义节点来扩展 ComfyUI 的功能。节点使用 Python 编写,并由社区发布。 - **扩展**:扩展是改进 ComfyUI 用户界面的第三方应用程序。 -- **[部署](/zh-CN/development/comfyui-server/comms_overview)**:ComfyUI 可以在您自己的环境中部署为 API 端点。 +- **[本地服务器 API](/zh-CN/development/comfyui-server/comms_overview)**:ComfyUI 可以在您自己的环境中部署为 API 端点。 +- **[Cloud API](/zh-CN/development/cloud/overview)**:通过 Comfy Cloud API 以编程方式运行工作流,无需管理自己的硬件。 diff --git a/zh-CN/get_started/cloud.mdx b/zh-CN/get_started/cloud.mdx index 93635f741..75297d7a8 100644 --- a/zh-CN/get_started/cloud.mdx +++ b/zh-CN/get_started/cloud.mdx @@ -34,7 +34,7 @@ ComfyUI Cloud 是 ComfyUI 的云端版本,具有与本地版本相同的功能 ## 云端版本 vs 本地版本 -目前 ComfyUI 有官方的云端版本 [Comfy Cloud](https://comfy.org/cloud) ,同时也有开源的本地部署版本, 如果你有足够强劲的 GPU,那么在本地部署并使用 ComfyUI 是一个不错的选择, Cloud 云端则是一个打开即用的在线应用,无需部署打开对应的 URL 即可访问使用 +目前 ComfyUI 有官方的云端版本 [Comfy Cloud](https://comfy.org/cloud),同时也有开源的本地部署版本。如果你有足够强劲的 GPU,那么在本地部署并使用 ComfyUI 是一个不错的选择;而 Comfy Cloud 则是一个开箱即用的在线应用,无需部署,只要打开对应的 URL 即可开始使用。 | 类别 | Comfy Cloud | 自托管(本地 ComfyUI) | |------|-------------|----------------------| @@ -79,7 +79,7 @@ ComfyUI Cloud 是 ComfyUI 的云端版本,具有与本地版本相同的功能 - 点击工作流我们的服务会开始为你的工作流分配机器,在队列面板中你可以了解对应工作流的执行状态 + 点击运行后,我们的服务会开始为你的工作流分配机器,你可以在队列面板中查看该工作流的执行状态。 ![查看队列](/images/cloud/workflow_check_queue.webp) @@ -132,6 +132,10 @@ ComfyUI Cloud 是 ComfyUI 的云端版本,具有与本地版本相同的功能 ## 下一步 + + 通过 Cloud API 以编程方式运行工作流 + + 探索教程以学习 ComfyUI 工作流 diff --git a/zh-CN/index.mdx b/zh-CN/index.mdx index a84bd4e84..11dff2711 100644 --- a/zh-CN/index.mdx +++ b/zh-CN/index.mdx @@ -132,11 +132,18 @@ sidebarTitle: "介绍" 创建和发布自定义节点 - 将 ComfyUI 集成到你的应用程序 + 集成本地 ComfyUI 服务器 + + + 通过 Comfy Cloud API 运行工作流 From dfcfb7a96b5ebb2bb123beec5321b017771ffd76 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:37:35 -0500 Subject: [PATCH 03/27] Address review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix branding in openapi-cloud.yaml: 'ComfyUI Cloud' → 'Comfy Cloud' - Fix zh-CN/development/cloud/openapi.mdx: revert /zh-CN/api-reference/cloud to /api-reference/cloud (OpenAPI reference is not localized) - Fix zh-CN/development/cloud/overview.mdx: update anchor links to match Chinese headings --- openapi-cloud.yaml | 10 +++++----- zh-CN/development/cloud/openapi.mdx | 2 +- zh-CN/development/cloud/overview.mdx | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/openapi-cloud.yaml b/openapi-cloud.yaml index e674f5e0a..d7f982563 100644 --- a/openapi-cloud.yaml +++ b/openapi-cloud.yaml @@ -1,22 +1,22 @@ openapi: 3.0.3 info: - title: ComfyUI Cloud API + title: Comfy Cloud API description: | **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. - API for ComfyUI Cloud - Run ComfyUI workflows on cloud infrastructure. + API for Comfy Cloud - Run ComfyUI workflows on cloud infrastructure. - This API allows you to interact with ComfyUI Cloud programmatically, including: + This API allows you to interact with Comfy Cloud programmatically, including: - Submitting and managing workflows - Uploading and downloading files - Monitoring job status and progress ## Cloud vs OSS ComfyUI Compatibility - ComfyUI Cloud implements the same API interfaces as OSS ComfyUI for maximum compatibility, + Comfy Cloud implements the same API interfaces as OSS ComfyUI for maximum compatibility, but some fields are accepted for compatibility while being handled differently or ignored: | Field | Endpoints | Cloud Behavior | @@ -35,7 +35,7 @@ info: servers: - url: https://cloud.comfy.org - description: ComfyUI Cloud API + description: Comfy Cloud API # All endpoints require API key authentication unless marked with security: [] security: diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index c99b24f40..7e9c39bfa 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -20,7 +20,7 @@ openapi: "/openapi-cloud.yaml" 通过交互式文档探索完整的 API: - + 浏览端点、查看模式并尝试请求 diff --git a/zh-CN/development/cloud/overview.mdx b/zh-CN/development/cloud/overview.mdx index d26ec7ce3..be811a2ea 100644 --- a/zh-CN/development/cloud/overview.mdx +++ b/zh-CN/development/cloud/overview.mdx @@ -164,12 +164,12 @@ print(f"Job submitted: {prompt_id}") | 类别 | 描述 | |----------|-------------| -| [工作流](/zh-CN/development/cloud/api-reference#running-workflows) | 提交工作流、检查状态 | -| [任务](/zh-CN/development/cloud/api-reference#checking-job-status) | 监控任务状态和队列 | -| [输入](/zh-CN/development/cloud/api-reference#uploading-inputs) | 上传图像、遮罩和其他输入 | -| [输出](/zh-CN/development/cloud/api-reference#downloading-outputs) | 下载生成的内容 | -| [WebSocket](/zh-CN/development/cloud/api-reference#websocket-for-real-time-progress) | 实时进度更新 | -| [对象信息](/zh-CN/development/cloud/api-reference#object-info) | 可用节点及其定义 | +| [工作流](/zh-CN/development/cloud/api-reference#运行工作流) | 提交工作流、检查状态 | +| [任务](/zh-CN/development/cloud/api-reference#检查任务状态) | 监控任务状态和队列 | +| [输入](/zh-CN/development/cloud/api-reference#上传输入) | 上传图像、遮罩和其他输入 | +| [输出](/zh-CN/development/cloud/api-reference#下载输出) | 下载生成的内容 | +| [WebSocket](/zh-CN/development/cloud/api-reference#实时进度-websocket) | 实时进度更新 | +| [对象信息](/zh-CN/development/cloud/api-reference#对象信息) | 可用节点及其定义 | ## 后续步骤 From bdb505c858e91e6bf0aab6e00cfa3a91fa905db7 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:40:54 -0500 Subject: [PATCH 04/27] Remove broken /api-reference/cloud links The interactive API reference cards linked to auto-generated OpenAPI paths that the link checker cannot verify. Removed these sections from both English and Chinese openapi.mdx files. --- development/cloud/openapi.mdx | 8 -------- zh-CN/development/cloud/openapi.mdx | 8 -------- 2 files changed, 16 deletions(-) diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx index 01e0ba853..dd37dca57 100644 --- a/development/cloud/openapi.mdx +++ b/development/cloud/openapi.mdx @@ -16,14 +16,6 @@ This page provides the complete OpenAPI 3.0 specification for the Comfy Cloud AP **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. -## Interactive API Reference - -Explore the full API with interactive documentation: - - - Browse endpoints, view schemas, and try requests - - ## Using the Specification The OpenAPI spec can be used to: diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index 7e9c39bfa..2011079ec 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -16,14 +16,6 @@ openapi: "/openapi-cloud.yaml" **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 -## 交互式 API 参考 - -通过交互式文档探索完整的 API: - - - 浏览端点、查看模式并尝试请求 - - ## 使用规范 OpenAPI 规范可用于: From b808ccc128d7a4f59effa5f390f783e0bd555d49 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:41:39 -0500 Subject: [PATCH 05/27] Revert "Remove broken /api-reference/cloud links" This reverts commit bdb505c858e91e6bf0aab6e00cfa3a91fa905db7. --- development/cloud/openapi.mdx | 8 ++++++++ zh-CN/development/cloud/openapi.mdx | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx index dd37dca57..01e0ba853 100644 --- a/development/cloud/openapi.mdx +++ b/development/cloud/openapi.mdx @@ -16,6 +16,14 @@ This page provides the complete OpenAPI 3.0 specification for the Comfy Cloud AP **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. +## Interactive API Reference + +Explore the full API with interactive documentation: + + + Browse endpoints, view schemas, and try requests + + ## Using the Specification The OpenAPI spec can be used to: diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index 2011079ec..7e9c39bfa 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -16,6 +16,14 @@ openapi: "/openapi-cloud.yaml" **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 +## 交互式 API 参考 + +通过交互式文档探索完整的 API: + + + 浏览端点、查看模式并尝试请求 + + ## 使用规范 OpenAPI 规范可用于: From 41d56af37020c6aa45304c1cb276feaa255c943e Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:43:08 -0500 Subject: [PATCH 06/27] Add Cloud API reference overview page to fix broken links - Create api-reference/cloud/overview.mdx as landing page - Update links in openapi.mdx files to point to /api-reference/cloud/overview --- api-reference/cloud/overview.mdx | 31 +++++++++++++++++++++++++++++ development/cloud/openapi.mdx | 2 +- zh-CN/development/cloud/openapi.mdx | 2 +- 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 api-reference/cloud/overview.mdx diff --git a/api-reference/cloud/overview.mdx b/api-reference/cloud/overview.mdx new file mode 100644 index 000000000..11fbc8d10 --- /dev/null +++ b/api-reference/cloud/overview.mdx @@ -0,0 +1,31 @@ +--- +title: "Cloud API Overview" +--- + + + **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. + + +The Comfy Cloud API provides programmatic access to run workflows on Comfy Cloud infrastructure. + + + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + + +## Getting Started + +- [Cloud API Overview](/development/cloud/overview) - Introduction, authentication, and quick start guide +- [API Reference](/development/cloud/api-reference) - Complete endpoint documentation with code examples +- [OpenAPI Specification](/development/cloud/openapi) - Machine-readable API specification + +## Endpoint Categories + +| Category | Description | +|----------|-------------| +| Workflow | Submit workflows for execution | +| Job | Monitor job status and manage the queue | +| Asset | Upload and download files | +| Model | Browse available AI models | +| Node | Get information about available nodes | +| User | Account information and personal data | +| System | Server status and health checks | diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx index 01e0ba853..db4a7ae49 100644 --- a/development/cloud/openapi.mdx +++ b/development/cloud/openapi.mdx @@ -20,7 +20,7 @@ This page provides the complete OpenAPI 3.0 specification for the Comfy Cloud AP Explore the full API with interactive documentation: - + Browse endpoints, view schemas, and try requests diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index 7e9c39bfa..90ed0f305 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -20,7 +20,7 @@ openapi: "/openapi-cloud.yaml" 通过交互式文档探索完整的 API: - + 浏览端点、查看模式并尝试请求 From 52e0fc28fcceae5b2e5aa71bafb7ed69413dc4e0 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:51:05 -0500 Subject: [PATCH 07/27] Add Chinese translation for api-reference/cloud/overview.mdx --- zh-CN/api-reference/cloud/overview.mdx | 31 ++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 zh-CN/api-reference/cloud/overview.mdx diff --git a/zh-CN/api-reference/cloud/overview.mdx b/zh-CN/api-reference/cloud/overview.mdx new file mode 100644 index 000000000..2fa5d7209 --- /dev/null +++ b/zh-CN/api-reference/cloud/overview.mdx @@ -0,0 +1,31 @@ +--- +title: "Cloud API 概述" +--- + + + **实验性 API:** 此 API 目前处于实验阶段,可能会发生变更。端点、请求/响应格式和行为可能会在不另行通知的情况下进行修改。 + + +Comfy Cloud API 提供以编程方式访问 Comfy Cloud 的能力,可在云端基础设施上运行工作流。 + + + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 + + +## 开始使用 + +- [Cloud API 概述](/zh-CN/development/cloud/overview) - 简介、认证和快速入门指南 +- [API 参考](/zh-CN/development/cloud/api-reference) - 完整的端点文档和代码示例 +- [OpenAPI 规范](/zh-CN/development/cloud/openapi) - 机器可读的 API 规范 + +## 端点类别 + +| 类别 | 描述 | +|----------|-------------| +| 工作流 | 提交工作流执行 | +| 任务 | 监控任务状态和管理队列 | +| 资源 | 上传和下载文件 | +| 模型 | 浏览可用的 AI 模型 | +| 节点 | 获取可用节点的信息 | +| 用户 | 账户信息和个人数据 | +| 系统 | 服务器状态和健康检查 | From 277d700e47d62f5c840edbb39f54303a07f84ae1 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:52:35 -0500 Subject: [PATCH 08/27] Add Cloud API Reference tab to Chinese navigation --- docs.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs.json b/docs.json index e71fcc753..dc6f01213 100644 --- a/docs.json +++ b/docs.json @@ -1369,6 +1369,13 @@ { "tab": "Registry API 参考文档", "openapi": "https://api.comfy.org/openapi" + }, + { + "tab": "Cloud API 参考文档", + "openapi": { + "source": "openapi-cloud.yaml", + "directory": "zh-CN/api-reference/cloud" + } } ] } From e8c3370c888ef57b16e2d1d5e3fb964fe1c92129 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 04:54:59 -0500 Subject: [PATCH 09/27] Add Cloud API sidebar group to Chinese development navigation --- docs.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs.json b/docs.json index dc6f01213..48a43c10b 100644 --- a/docs.json +++ b/docs.json @@ -1224,6 +1224,15 @@ "zh-CN/development/comfyui-server/api-key-integration" ] }, + { + "group": "Cloud API", + "icon": "cloud", + "pages": [ + "zh-CN/development/cloud/overview", + "zh-CN/development/cloud/api-reference", + "zh-CN/development/cloud/openapi" + ] + }, { "group": "CLI", "pages": [ From 21529da3f7379474ce1deb2b2dbf1b98b51c625d Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Thu, 1 Jan 2026 23:47:56 -0500 Subject: [PATCH 10/27] fix: align zh-CN navigation with English version - Fix link in zh-CN/development/cloud/openapi.mdx to point to Chinese API reference - Add missing zh-CN/registry/claim-my-node to Chinese navigation --- docs.json | 1 + zh-CN/development/cloud/openapi.mdx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs.json b/docs.json index 48a43c10b..e7e9975e1 100644 --- a/docs.json +++ b/docs.json @@ -1293,6 +1293,7 @@ "pages": [ "zh-CN/registry/overview", "zh-CN/registry/publishing", + "zh-CN/registry/claim-my-node", "zh-CN/registry/standards", "zh-CN/registry/cicd", "zh-CN/registry/specifications" diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index 90ed0f305..c97bdcd05 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -20,7 +20,7 @@ openapi: "/openapi-cloud.yaml" 通过交互式文档探索完整的 API: - + 浏览端点、查看模式并尝试请求 From a03e3d956099a6b208a66f2e10e5d54e9755bb52 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Mon, 5 Jan 2026 12:55:03 -0500 Subject: [PATCH 11/27] docs: add extra_data documentation for Partner Nodes API key Document how to pass api_key_comfy_org in extra_data when workflows contain Partner Nodes (Flux Pro, Ideogram, etc.). Added to both English and Chinese API reference docs. --- development/cloud/api-reference.mdx | 80 +++++++++++++++++++++++ openapi-cloud.yaml | 42 +++++++----- zh-CN/development/cloud/api-reference.mdx | 80 +++++++++++++++++++++++ 3 files changed, 186 insertions(+), 16 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 407e93b25..755ff0e26 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -415,6 +415,86 @@ print(f"Submitted job: {prompt_id}") ``` +### Using Partner Nodes + +If your workflow contains [Partner Nodes](/tutorials/api-nodes/overview) (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. + + + +```bash curl +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" + } + }' +``` + +```typescript TypeScript +async function submitWorkflowWithApiNodes( + workflow: Record, + comfyApiKey: string +): Promise { + const response = await fetch(`${BASE_URL}/api/prompt`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ + prompt: workflow, + extra_data: { + api_key_comfy_org: comfyApiKey, + }, + }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const result = await response.json(); + return result.prompt_id; +} + +// Use when workflow contains API nodes (e.g., Flux Pro, Ideogram, etc.) +const COMFY_API_KEY = process.env.COMFY_API_KEY!; +const promptId = await submitWorkflowWithApiNodes(workflow, COMFY_API_KEY); +``` + +```python Python +def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: + """Submit a workflow that uses API nodes (partner nodes). + + Args: + workflow: ComfyUI workflow in API format + comfy_api_key: Your Comfy API key from platform.comfy.org + + Returns: + prompt_id for tracking the job + """ + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={ + "prompt": workflow, + "extra_data": { + "api_key_comfy_org": comfy_api_key + } + } + ) + response.raise_for_status() + return response.json()["prompt_id"] + +# Use when workflow contains API nodes +COMFY_API_KEY = os.environ["COMFY_API_KEY"] +prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) +``` + + + + Generate a Comfy API key at [platform.comfy.org](https://platform.comfy.org/login). This key is separate from your Cloud API key and is used to authenticate with partner AI services. + + ### Modify Workflow Inputs diff --git a/openapi-cloud.yaml b/openapi-cloud.yaml index d7f982563..f00c797e4 100644 --- a/openapi-cloud.yaml +++ b/openapi-cloud.yaml @@ -2620,26 +2620,36 @@ components: type: array description: Array of currently running job items items: - type: array - description: | - Queue item tuple format: [job_number, prompt_id, workflow_json, output_node_ids, metadata] - - [0] job_number (integer): Position in queue (1-based) - - [1] prompt_id (string): Job UUID - - [2] workflow_json (object): Full ComfyUI workflow - - [3] output_node_ids (array): Node IDs to return results from - - [4] metadata (object): Contains {create_time: } + $ref: '#/components/schemas/QueueItem' queue_pending: type: array description: Array of pending job items (ordered by creation time, oldest first) items: - type: array - description: | - Queue item tuple format: [job_number, prompt_id, workflow_json, output_node_ids, metadata] - - [0] job_number (integer): Position in queue (1-based) - - [1] prompt_id (string): Job UUID - - [2] workflow_json (object): Full ComfyUI workflow - - [3] output_node_ids (array): Node IDs to return results from - - [4] metadata (object): Contains {create_time: } + $ref: '#/components/schemas/QueueItem' + + QueueItem: + type: array + description: | + Queue item tuple format: [job_number, prompt_id, workflow_json, output_node_ids, metadata] + - [0] job_number (integer): Position in queue (1-based) + - [1] prompt_id (string): Job UUID + - [2] workflow_json (object): Full ComfyUI workflow + - [3] output_node_ids (array): Node IDs to return results from + - [4] metadata (object): Contains {create_time: } + items: + oneOf: + - type: integer + description: Job number (position in queue) + - type: string + description: Prompt ID (UUID) + - type: object + description: Workflow JSON + - type: array + items: + type: string + description: Output node IDs + - type: object + description: Metadata with create_time QueueManageRequest: type: object diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 51f430160..5ac113bc5 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -415,6 +415,86 @@ print(f"Submitted job: {prompt_id}") ``` +### 使用合作伙伴节点 + +如果您的工作流包含[合作伙伴节点](/zh-CN/tutorials/api-nodes/overview)(调用外部 AI 服务的节点,如 Flux Pro、Ideogram 等),您必须在请求体的 `extra_data` 字段中包含您的 Comfy API 密钥。 + + + 在浏览器中运行工作流时,ComfyUI 前端会自动将您的 API 密钥打包到 `extra_data` 中。本节仅适用于直接调用 API 的情况。 + + + +```bash curl +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" + } + }' +``` + +```typescript TypeScript +async function submitWorkflowWithApiNodes( + workflow: Record, + comfyApiKey: string +): Promise { + const response = await fetch(`${BASE_URL}/api/prompt`, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify({ + prompt: workflow, + extra_data: { + api_key_comfy_org: comfyApiKey, + }, + }), + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + const result = await response.json(); + return result.prompt_id; +} + +// 当工作流包含 API 节点时使用(例如 Flux Pro、Ideogram 等) +const COMFY_API_KEY = process.env.COMFY_API_KEY!; +const promptId = await submitWorkflowWithApiNodes(workflow, COMFY_API_KEY); +``` + +```python Python +def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: + """提交使用 API 节点(合作伙伴节点)的工作流。 + + Args: + workflow: API 格式的 ComfyUI 工作流 + comfy_api_key: 来自 platform.comfy.org 的 Comfy API 密钥 + + Returns: + 用于跟踪任务的 prompt_id + """ + response = requests.post( + f"{BASE_URL}/api/prompt", + headers=get_headers(), + json={ + "prompt": workflow, + "extra_data": { + "api_key_comfy_org": comfy_api_key + } + } + ) + response.raise_for_status() + return response.json()["prompt_id"] + +# 当工作流包含 API 节点时使用 +COMFY_API_KEY = os.environ["COMFY_API_KEY"] +prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) +``` + + + + 在 [platform.comfy.org](https://platform.comfy.org/login) 生成 Comfy API 密钥。此密钥与您的 Cloud API 密钥不同,用于与合作伙伴 AI 服务进行身份验证。 + + ### 修改工作流输入 From 66bdf528d7d7b08fdde4f8efb3c29a0a2494b119 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Mon, 5 Jan 2026 12:59:09 -0500 Subject: [PATCH 12/27] docs: fix API key info - same key for Cloud API and Partner Nodes --- development/cloud/api-reference.mdx | 2 +- zh-CN/development/cloud/api-reference.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 755ff0e26..29a678dad 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -492,7 +492,7 @@ prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) - Generate a Comfy API key at [platform.comfy.org](https://platform.comfy.org/login). This key is separate from your Cloud API key and is used to authenticate with partner AI services. + Generate your API key at [platform.comfy.org](https://platform.comfy.org/login). This is the same key used for Cloud API authentication (`X-API-Key` header). ### Modify Workflow Inputs diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 5ac113bc5..acbeaea6c 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -492,7 +492,7 @@ prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) - 在 [platform.comfy.org](https://platform.comfy.org/login) 生成 Comfy API 密钥。此密钥与您的 Cloud API 密钥不同,用于与合作伙伴 AI 服务进行身份验证。 + 在 [platform.comfy.org](https://platform.comfy.org/login) 生成您的 API 密钥。此密钥与 Cloud API 身份验证(`X-API-Key` 请求头)使用的是同一个密钥。 ### 修改工作流输入 From 52a61a5de916f145c36741f90a8bf88c6cc414bb Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Mon, 5 Jan 2026 13:00:43 -0500 Subject: [PATCH 13/27] docs: use same API_KEY env var for Partner Nodes examples --- development/cloud/api-reference.mdx | 24 +++++++++++------------ zh-CN/development/cloud/api-reference.mdx | 24 +++++++++++------------ 2 files changed, 22 insertions(+), 26 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 29a678dad..e0e42edf3 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -437,9 +437,9 @@ curl -X POST "$BASE_URL/api/prompt" \ ``` ```typescript TypeScript -async function submitWorkflowWithApiNodes( +async function submitWorkflowWithPartnerNodes( workflow: Record, - comfyApiKey: string + apiKey: string ): Promise { const response = await fetch(`${BASE_URL}/api/prompt`, { method: "POST", @@ -447,7 +447,7 @@ async function submitWorkflowWithApiNodes( body: JSON.stringify({ prompt: workflow, extra_data: { - api_key_comfy_org: comfyApiKey, + api_key_comfy_org: apiKey, }, }), }); @@ -456,18 +456,17 @@ async function submitWorkflowWithApiNodes( return result.prompt_id; } -// Use when workflow contains API nodes (e.g., Flux Pro, Ideogram, etc.) -const COMFY_API_KEY = process.env.COMFY_API_KEY!; -const promptId = await submitWorkflowWithApiNodes(workflow, COMFY_API_KEY); +// Use when workflow contains Partner Nodes (e.g., Flux Pro, Ideogram, etc.) +const promptId = await submitWorkflowWithPartnerNodes(workflow, API_KEY); ``` ```python Python -def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: - """Submit a workflow that uses API nodes (partner nodes). +def submit_workflow_with_partner_nodes(workflow: dict, api_key: str) -> str: + """Submit a workflow that uses Partner Nodes. Args: workflow: ComfyUI workflow in API format - comfy_api_key: Your Comfy API key from platform.comfy.org + api_key: Your API key from platform.comfy.org Returns: prompt_id for tracking the job @@ -478,16 +477,15 @@ def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: json={ "prompt": workflow, "extra_data": { - "api_key_comfy_org": comfy_api_key + "api_key_comfy_org": api_key } } ) response.raise_for_status() return response.json()["prompt_id"] -# Use when workflow contains API nodes -COMFY_API_KEY = os.environ["COMFY_API_KEY"] -prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) +# Use when workflow contains Partner Nodes +prompt_id = submit_workflow_with_partner_nodes(workflow, API_KEY) ``` diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index acbeaea6c..9bce6f7f5 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -437,9 +437,9 @@ curl -X POST "$BASE_URL/api/prompt" \ ``` ```typescript TypeScript -async function submitWorkflowWithApiNodes( +async function submitWorkflowWithPartnerNodes( workflow: Record, - comfyApiKey: string + apiKey: string ): Promise { const response = await fetch(`${BASE_URL}/api/prompt`, { method: "POST", @@ -447,7 +447,7 @@ async function submitWorkflowWithApiNodes( body: JSON.stringify({ prompt: workflow, extra_data: { - api_key_comfy_org: comfyApiKey, + api_key_comfy_org: apiKey, }, }), }); @@ -456,18 +456,17 @@ async function submitWorkflowWithApiNodes( return result.prompt_id; } -// 当工作流包含 API 节点时使用(例如 Flux Pro、Ideogram 等) -const COMFY_API_KEY = process.env.COMFY_API_KEY!; -const promptId = await submitWorkflowWithApiNodes(workflow, COMFY_API_KEY); +// 当工作流包含合作伙伴节点时使用(例如 Flux Pro、Ideogram 等) +const promptId = await submitWorkflowWithPartnerNodes(workflow, API_KEY); ``` ```python Python -def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: - """提交使用 API 节点(合作伙伴节点)的工作流。 +def submit_workflow_with_partner_nodes(workflow: dict, api_key: str) -> str: + """提交使用合作伙伴节点的工作流。 Args: workflow: API 格式的 ComfyUI 工作流 - comfy_api_key: 来自 platform.comfy.org 的 Comfy API 密钥 + api_key: 来自 platform.comfy.org 的 API 密钥 Returns: 用于跟踪任务的 prompt_id @@ -478,16 +477,15 @@ def submit_workflow_with_api_nodes(workflow: dict, comfy_api_key: str) -> str: json={ "prompt": workflow, "extra_data": { - "api_key_comfy_org": comfy_api_key + "api_key_comfy_org": api_key } } ) response.raise_for_status() return response.json()["prompt_id"] -# 当工作流包含 API 节点时使用 -COMFY_API_KEY = os.environ["COMFY_API_KEY"] -prompt_id = submit_workflow_with_api_nodes(workflow, COMFY_API_KEY) +# 当工作流包含合作伙伴节点时使用 +prompt_id = submit_workflow_with_partner_nodes(workflow, API_KEY) ``` From e4b99e563d4237688e48ca35b3481f00c30f71f1 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:04:04 -0500 Subject: [PATCH 14/27] docs: clarify /api/view returns temporary signed URL redirect --- development/cloud/overview.mdx | 2 +- zh-CN/development/cloud/overview.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/development/cloud/overview.mdx b/development/cloud/overview.mdx index 39df8d456..30a7b5518 100644 --- a/development/cloud/overview.mdx +++ b/development/cloud/overview.mdx @@ -104,7 +104,7 @@ When you submit a workflow, a **job** is created. Jobs are executed asynchronous ### Outputs -Generated content (images, videos, audio) is stored in cloud storage. Output files can be downloaded via the `/api/view` endpoint or through signed URLs. +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 diff --git a/zh-CN/development/cloud/overview.mdx b/zh-CN/development/cloud/overview.mdx index be811a2ea..a7b5cc108 100644 --- a/zh-CN/development/cloud/overview.mdx +++ b/zh-CN/development/cloud/overview.mdx @@ -104,7 +104,7 @@ ComfyUI 工作流是描述节点图的 JSON 对象。API 接受"API 格式"的 ### 输出 -生成的内容(图像、视频、音频)存储在云存储中。输出文件可通过 `/api/view` 端点或签名 URL 下载。 +生成的内容(图像、视频、音频)存储在云存储中。输出文件可通过 `/api/view` 端点下载,该端点会返回 302 重定向到临时签名 URL。 ## 快速入门 From f63a0142a8f69b5ae9e92b86b7d6ef3266923859 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:08:29 -0500 Subject: [PATCH 15/27] docs: add utm_campaign=cloud-api to pricing links --- development/cloud/api-reference.mdx | 2 +- development/cloud/openapi.mdx | 2 +- development/cloud/overview.mdx | 2 +- zh-CN/development/cloud/api-reference.mdx | 2 +- zh-CN/development/cloud/openapi.mdx | 2 +- zh-CN/development/cloud/overview.mdx | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index e0e42edf3..bbfd173cf 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -12,7 +12,7 @@ description: "Complete API reference with code examples for Comfy Cloud" 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](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api) for details. ## Setup diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx index db4a7ae49..821774338 100644 --- a/development/cloud/openapi.mdx +++ b/development/cloud/openapi.mdx @@ -13,7 +13,7 @@ openapi: "/openapi-cloud.yaml" This page provides the complete OpenAPI 3.0 specification for the Comfy Cloud API. - **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api) for details. ## Interactive API Reference diff --git a/development/cloud/overview.mdx b/development/cloud/overview.mdx index 30a7b5518..aabf9bd5e 100644 --- a/development/cloud/overview.mdx +++ b/development/cloud/overview.mdx @@ -12,7 +12,7 @@ description: "Programmatic access to Comfy Cloud for running workflows, managing 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:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs) for details. + **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api) for details. ## Base URL diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 9bce6f7f5..a1423187f 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -12,7 +12,7 @@ description: "Comfy Cloud 的完整 API 参考及代码示例" 本页面提供了常见 Comfy Cloud API 操作的完整示例。 - **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。请查看[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)了解详情。 + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。请查看[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api)了解详情。 ## 设置 diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx index c97bdcd05..1f9a32cef 100644 --- a/zh-CN/development/cloud/openapi.mdx +++ b/zh-CN/development/cloud/openapi.mdx @@ -13,7 +13,7 @@ openapi: "/openapi-cloud.yaml" 本页面提供 Comfy Cloud API 的完整 OpenAPI 3.0 规范。 - **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api)。 ## 交互式 API 参考 diff --git a/zh-CN/development/cloud/overview.mdx b/zh-CN/development/cloud/overview.mdx index a7b5cc108..38bc7ffa7 100644 --- a/zh-CN/development/cloud/overview.mdx +++ b/zh-CN/development/cloud/overview.mdx @@ -12,7 +12,7 @@ description: "通过编程方式访问 Comfy Cloud,在云端运行工作流、 Comfy Cloud API 提供以编程方式访问 Comfy Cloud 的能力,可在云端基础设施上运行工作流。该 API 与本地 ComfyUI 的 API 兼容,便于迁移现有集成。 - **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs)。 + **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。详情请参阅[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api)。 ## 基础 URL From 94100531be52604ab30ab94409c0004cce440c13 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:13:56 -0500 Subject: [PATCH 16/27] docs: fix Python f-string syntax in object_info example --- development/cloud/api-reference.mdx | 3 ++- zh-CN/development/cloud/api-reference.mdx | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index bbfd173cf..9426565a4 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -102,7 +102,8 @@ print(f"Available nodes: {len(object_info)}") # Get a specific node's definition ksampler = object_info.get("KSampler", {}) -print(f"KSampler inputs: {list(ksampler.get('input', {}).get('required', {}).keys())}") +inputs = list(ksampler.get('input', {}).get('required', {}).keys()) +print(f"KSampler inputs: {inputs}") ``` diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index a1423187f..69d83e097 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -102,7 +102,8 @@ print(f"Available nodes: {len(object_info)}") # Get a specific node's definition ksampler = object_info.get("KSampler", {}) -print(f"KSampler inputs: {list(ksampler.get('input', {}).get('required', {}).keys())}") +inputs = list(ksampler.get('input', {}).get('required', {}).keys()) +print(f"KSampler inputs: {inputs}") ``` From 250f47b2c96ca86531bec47650080f40df0e9895 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:26:45 -0500 Subject: [PATCH 17/27] docs: fix download outputs examples to handle 302 redirect --- development/cloud/api-reference.mdx | 16 +++++++++++----- zh-CN/development/cloud/api-reference.mdx | 16 +++++++++++----- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 9426565a4..fbcc38217 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -870,8 +870,8 @@ Retrieve generated files after job completion. ```bash curl -# Download a single output file -curl -X GET "$BASE_URL/api/view?filename=output.png&subfolder=&type=output" \ +# 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 ``` @@ -883,11 +883,17 @@ async function downloadOutput( outputType: string = "output" ): Promise { const params = new URLSearchParams({ filename, subfolder, type: outputType }); + // Get the redirect URL (don't follow automatically to avoid sending auth to storage) const response = await fetch(`${BASE_URL}/api/view?${params}`, { - headers: getHeaders(), + headers: { "X-API-Key": API_KEY }, + redirect: "manual", }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.arrayBuffer(); + 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}`); + return fileResponse.arrayBuffer(); } async function saveOutputs( diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 69d83e097..bdb4ec63f 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -870,8 +870,8 @@ outputs = asyncio.run(run_with_websocket()) ```bash curl -# Download a single output file -curl -X GET "$BASE_URL/api/view?filename=output.png&subfolder=&type=output" \ +# 下载单个输出文件(使用 -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 ``` @@ -883,11 +883,17 @@ async function downloadOutput( outputType: string = "output" ): Promise { const params = new URLSearchParams({ filename, subfolder, type: outputType }); + // Get the redirect URL (don't follow automatically to avoid sending auth to storage) const response = await fetch(`${BASE_URL}/api/view?${params}`, { - headers: getHeaders(), + headers: { "X-API-Key": API_KEY }, + redirect: "manual", }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.arrayBuffer(); + 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}`); + return fileResponse.arrayBuffer(); } async function saveOutputs( From 9c151c102bc1bac73caa23c688318bca99f8f1dc Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:41:48 -0500 Subject: [PATCH 18/27] docs: fix job status endpoint and polling logic - Changed endpoint from /api/job/{id}/status to /api/jobs/{id} - Use top-level 'status' field (not execution_status.status_str) - Check for 'completed' status (not 'success') --- development/cloud/api-reference.mdx | 49 ++++++++++++----------- zh-CN/development/cloud/api-reference.mdx | 49 ++++++++++++----------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index fbcc38217..8db44026b 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -543,23 +543,24 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") ## Checking Job Status -Poll for job completion status. +Poll for job completion and retrieve outputs. ```bash curl -# Get job status -curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ +# Get full job details including outputs +curl -X GET "$BASE_URL/api/jobs/{prompt_id}" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ``` ```typescript TypeScript -interface JobStatus { +interface JobResponse { + id: string; status: string; outputs?: Record; } -async function getJobStatus(promptId: string): Promise { - const response = await fetch(`${BASE_URL}/api/job/${promptId}/status`, { +async function getJob(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/jobs/${promptId}`, { headers: getHeaders(), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -570,16 +571,16 @@ async function pollForCompletion( promptId: string, timeout: number = 300, pollInterval: number = 2000 -): Promise { +): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout * 1000) { - const status = await getJobStatus(promptId); + const job = await getJob(promptId); - if (status.status === "completed") { - return status; - } else if (["error", "failed", "cancelled"].includes(status.status)) { - throw new Error(`Job failed with status: ${status.status}`); + if (job.status === "completed") { + return job; + } else if (["error", "failed", "cancelled"].includes(job.status)) { + throw new Error(`Job failed with status: ${job.status}`); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); @@ -593,14 +594,14 @@ console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); ``` ```python Python -def get_job_status(prompt_id: str) -> dict: - """Get the status of a job. +def get_job(prompt_id: str) -> dict: + """Get full job details including outputs. Returns: - Job status with fields: status, outputs (if complete) + Job data with fields: id, status, outputs """ response = requests.get( - f"{BASE_URL}/api/job/{prompt_id}/status", + f"{BASE_URL}/api/jobs/{prompt_id}", headers=get_headers() ) response.raise_for_status() @@ -615,18 +616,18 @@ def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float poll_interval: Seconds between polls Returns: - Final job status + Final job data with outputs """ start_time = time.time() while time.time() - start_time < timeout: - status = get_job_status(prompt_id) - job_status = status.get("status", "unknown") + job = get_job(prompt_id) + status = job.get("status") - if job_status == "completed": - return status - elif job_status in ("error", "failed", "cancelled"): - raise RuntimeError(f"Job failed with status: {job_status}") + if status == "completed": + return job + elif status in ("error", "failed", "cancelled"): + raise RuntimeError(f"Job failed with status: {status}") # Still pending or in_progress time.sleep(poll_interval) @@ -635,7 +636,7 @@ def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float # Wait for job to complete result = poll_for_completion(prompt_id) -print(f"Job completed! Outputs: {result.get('outputs', {}).keys()}") +print(f"Job completed! Outputs: {list(result.get('outputs', {}).keys())}") ``` diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index bdb4ec63f..c9fb168f1 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -543,23 +543,24 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") ## 检查任务状态 -轮询任务完成状态。 +轮询任务完成状态并获取输出。 ```bash curl -# Get job status -curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ +# 获取完整任务详情(包括输出) +curl -X GET "$BASE_URL/api/jobs/{prompt_id}" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ``` ```typescript TypeScript -interface JobStatus { +interface JobResponse { + id: string; status: string; outputs?: Record; } -async function getJobStatus(promptId: string): Promise { - const response = await fetch(`${BASE_URL}/api/job/${promptId}/status`, { +async function getJob(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/jobs/${promptId}`, { headers: getHeaders(), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -570,16 +571,16 @@ async function pollForCompletion( promptId: string, timeout: number = 300, pollInterval: number = 2000 -): Promise { +): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout * 1000) { - const status = await getJobStatus(promptId); + const job = await getJob(promptId); - if (status.status === "completed") { - return status; - } else if (["error", "failed", "cancelled"].includes(status.status)) { - throw new Error(`Job failed with status: ${status.status}`); + if (job.status === "completed") { + return job; + } else if (["error", "failed", "cancelled"].includes(job.status)) { + throw new Error(`Job failed with status: ${job.status}`); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); @@ -593,14 +594,14 @@ console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); ``` ```python Python -def get_job_status(prompt_id: str) -> dict: - """Get the status of a job. +def get_job(prompt_id: str) -> dict: + """Get full job details including outputs. Returns: - Job status with fields: status, outputs (if complete) + Job data with fields: id, status, outputs """ response = requests.get( - f"{BASE_URL}/api/job/{prompt_id}/status", + f"{BASE_URL}/api/jobs/{prompt_id}", headers=get_headers() ) response.raise_for_status() @@ -615,18 +616,18 @@ def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float poll_interval: Seconds between polls Returns: - Final job status + Final job data with outputs """ start_time = time.time() while time.time() - start_time < timeout: - status = get_job_status(prompt_id) - job_status = status.get("status", "unknown") + job = get_job(prompt_id) + status = job.get("status") - if job_status == "completed": - return status - elif job_status in ("error", "failed", "cancelled"): - raise RuntimeError(f"Job failed with status: {job_status}") + if status == "completed": + return job + elif status in ("error", "failed", "cancelled"): + raise RuntimeError(f"Job failed with status: {status}") # Still pending or in_progress time.sleep(poll_interval) @@ -635,7 +636,7 @@ def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float # Wait for job to complete result = poll_for_completion(prompt_id) -print(f"Job completed! Outputs: {result.get('outputs', {}).keys()}") +print(f"Job completed! Outputs: {list(result.get('outputs', {}).keys())}") ``` From 681fa07cc9241a24756ef2514b5b64690acbe49b Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:51:12 -0500 Subject: [PATCH 19/27] docs: document two job status endpoints and their different status values - /api/jobs/{id} returns user-friendly status (pending, in_progress, completed, etc.) - /api/job/{id}/status returns raw internal states (success, executing, etc.) - Added note recommending /api/jobs/{id} for polling --- development/cloud/api-reference.mdx | 6 ++++++ zh-CN/development/cloud/api-reference.mdx | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 8db44026b..44433693d 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -545,6 +545,12 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") Poll for job completion and retrieve outputs. + + The API provides two endpoints for checking job status: + - **`GET /api/jobs/{id}`** - Returns user-friendly status (`pending`, `in_progress`, `completed`, `failed`, `cancelled`) along with outputs. **Recommended for polling.** + - **`GET /api/job/{id}/status`** - Returns raw internal states (e.g., `success`, `executing`, `queued_limited`). Useful for debugging but not recommended for polling logic. + + ```bash curl # Get full job details including outputs diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index c9fb168f1..77f6896f3 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -545,6 +545,12 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") 轮询任务完成状态并获取输出。 + + API 提供两个端点用于检查任务状态: + - **`GET /api/jobs/{id}`** - 返回用户友好的状态(`pending`、`in_progress`、`completed`、`failed`、`cancelled`)以及输出。**推荐用于轮询。** + - **`GET /api/job/{id}/status`** - 返回原始内部状态(如 `success`、`executing`、`queued_limited`)。适用于调试,但不建议用于轮询逻辑。 + + ```bash curl # 获取完整任务详情(包括输出) From cee0d07dee309456bdf8787eea4eca51a58fc31b Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 11:54:23 -0500 Subject: [PATCH 20/27] docs: add recommendations for cloud team re: API discrepancies --- CLOUD_API_RECOMMENDATIONS.md | 118 +++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 CLOUD_API_RECOMMENDATIONS.md diff --git a/CLOUD_API_RECOMMENDATIONS.md b/CLOUD_API_RECOMMENDATIONS.md new file mode 100644 index 000000000..74930bc1d --- /dev/null +++ b/CLOUD_API_RECOMMENDATIONS.md @@ -0,0 +1,118 @@ +# Cloud API Recommendations + +This document summarizes issues discovered during API documentation testing that should be addressed in the cloud repo. + +## 1. OpenAPI Spec: JobStatusResponse Status Enum Mismatch + +**Location:** `cloud/services/ingest/openapi.yaml` line 3393-3396 + +**Issue:** The `JobStatusResponse` schema defines status enum as: +```yaml +enum: [waiting_to_dispatch, pending, in_progress, completed, error, cancelled] +``` + +But the actual `GetJobStatus` implementation (line 369 in `job.go`) directly casts the internal state: +```go +Status: ingest.JobStatusResponseStatus(jobEntity.Status) +``` + +This means the API returns raw internal states like `success`, `queued_limited`, `executing`, etc. — none of which are in the OpenAPI enum. + +**Recommendation:** Either: +1. Update OpenAPI to list all actual internal states returned, OR +2. Transform the status in `GetJobStatus` using `toFilterStatus()` like `GetJobDetail` does + +The latter would make both endpoints consistent, but would be a breaking change for any clients relying on the current behavior. + +--- + +## 2. Inconsistent Status Values Between Endpoints + +| Endpoint | Status for successful job | Includes outputs? | +|----------|---------------------------|-------------------| +| `GET /api/jobs/{id}` | `completed` | ✅ Yes | +| `GET /api/job/{id}/status` | `success` | ❌ No | + +**Current behavior:** +- `/api/jobs/{id}` uses `toFilterStatus()` which maps `StateSuccess` → `completed` +- `/api/job/{id}/status` returns raw `jobEntity.Status` directly + +This is confusing for API consumers who might expect consistent status values. + +**Recommendation:** Document this clearly (done in API docs) and consider whether `/api/job/{id}/status` should be deprecated in favor of `/api/jobs/{id}`. + +--- + +## 3. Internal State Mapping Reference + +For documentation purposes, here's how internal states map to user-friendly statuses: + +### Pending States → `pending` +- `submitted` +- `queued_limited` +- `queued_waiting` +- `allocated` +- `preparing` +- `pending_execution` + +### In Progress States → `in_progress` +- `executing` +- `executed` + +### Success State → `completed` +- `success` + +### Failed States → `failed` +- `error` +- `non_retryable_error` +- `erroring` +- `lost` + +### Cancelled States → `cancelled` +- `cancelled` +- `cancel_requested` +- `cancel_pending` +- `cancelling_preparing` + +--- + +## 4. Suggested OpenAPI Fix + +If choosing to document actual behavior (option 1), update `JobStatusResponse.status` enum to: + +```yaml +status: + type: string + enum: + # Pending states + - submitted + - queued_limited + - queued_waiting + - allocated + - preparing + - pending_execution + # In progress states + - executing + - executed + # Terminal states + - success + - error + - non_retryable_error + - erroring + - lost + - cancelled + - cancel_requested + - cancel_pending + - cancelling_preparing + description: | + Raw internal job state. For user-friendly status values, use GET /api/jobs/{id} instead + which returns: pending, in_progress, completed, failed, cancelled. +``` + +--- + +## 5. Related Files + +- `cloud/services/ingest/openapi.yaml` - OpenAPI spec +- `cloud/services/ingest/server/implementation/job.go` - GetJobStatus, GetJobDetail implementations +- `cloud/common/jobstate/state.go` - State definitions and groupings From 8232a123d4b19679408844ca34a051265ce4378e Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:10:36 -0500 Subject: [PATCH 21/27] Remove untested from-hash API, fix download redirect handling - Remove Reference Well-Known Assets section (from-hash API not usable as documented - hash format mismatch between upload and from-hash) - Add note about subfolder being ignored in Upload Mask (cloud uses flat content-addressed storage) - Fix downloadOutputs in End-to-End example to use redirect:manual to avoid sending auth headers to GCS signed URLs Testing confirmed: - Partner nodes with extra_data work correctly - Cancel job and interrupt execution work - Upload mask works (subfolder ignored) - from-hash endpoint exists but requires blake3: format hashes that aren't returned by any upload API --- development/cloud/api-reference.mdx | 117 +++------------------- zh-CN/development/cloud/api-reference.mdx | 117 +++------------------- 2 files changed, 26 insertions(+), 208 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 44433693d..f4a79f246 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -180,6 +180,10 @@ print(f"Uploaded: {result['name']} to {result['subfolder']}") ### 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. + + ```bash curl curl -X POST "$BASE_URL/api/upload/mask" \ @@ -246,107 +250,6 @@ def upload_mask(file_path: str, original_ref: dict) -> dict: ``` -### Reference Well-Known Assets (Skip Upload) - -If you know a file already exists in cloud storage (e.g., a popular sample image shared by Comfy), you can create an asset reference without uploading any bytes. First check if the hash exists, then create your own reference to it. - - -```bash curl -# 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" - }' -``` - -```typescript TypeScript -async function checkHashExists(hash: string): Promise { - // Note: /api/assets/hash/{hash} only accepts blake3: format - const response = await fetch(`${BASE_URL}/api/assets/hash/${hash}`, { - method: "HEAD", - }); - return response.ok; -} - -async function createAssetFromHash( - hash: string, - name: string, - tags: string[] = ["input"] -): Promise<{ id: string; hash: string }> { - // Note: /api/assets/from-hash accepts both blake3: and sha256: formats - const response = await fetch(`${BASE_URL}/api/assets/from-hash`, { - method: "POST", - headers: getHeaders(), - body: JSON.stringify({ hash, name, tags }), - }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json(); -} - -// Use a well-known sample image without uploading -// Hash format: blake3:<64 hex chars> -const sampleHash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"; - -if (await checkHashExists(sampleHash)) { - const asset = await createAssetFromHash(sampleHash, "sample_portrait.png"); - console.log(`Created asset ${asset.id} pointing to ${asset.hash}`); - - // Use in workflow (assuming workflow is already loaded) - // workflow["1"]["inputs"]["image"] = asset.hash; -} -``` - -```python Python -def check_hash_exists(hash: str) -> bool: - """Check if a file hash exists in cloud storage (unauthenticated). - - Note: This endpoint only accepts blake3: format hashes. - """ - response = requests.head(f"{BASE_URL}/api/assets/hash/{hash}") - return response.status_code == 200 - -def create_asset_from_hash(hash: str, name: str, tags: list = None) -> dict: - """Create an asset reference from an existing hash. - - This skips uploading bytes entirely - useful for well-known files - or files you've previously uploaded to the cloud. - - Note: This endpoint accepts both blake3: and sha256: format hashes. - """ - response = requests.post( - f"{BASE_URL}/api/assets/from-hash", - headers=get_headers(), - json={ - "hash": hash, - "name": name, - "tags": tags or ["input"] - } - ) - response.raise_for_status() - return response.json() - -# Use a well-known sample image without uploading -# Hash format: blake3:<64 hex chars> or sha256:<64 hex chars> -sample_hash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" - -if check_hash_exists(sample_hash): - asset = create_asset_from_hash(sample_hash, "sample_portrait.png") - print(f"Created asset {asset['id']} pointing to {asset['hash']}") - - # Use in workflow - workflow["1"]["inputs"]["image"] = asset["hash"] -``` - - --- ## Running Workflows @@ -1076,13 +979,19 @@ async function downloadOutputs( 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: getHeaders(), + headers: { "X-API-Key": API_KEY }, + redirect: "manual", }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + 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 response.arrayBuffer())); + await writeFile(path, Buffer.from(await fileResponse.arrayBuffer())); console.log(`Downloaded: ${path}`); } } diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 77f6896f3..fe04b9367 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -180,6 +180,10 @@ print(f"Uploaded: {result['name']} to {result['subfolder']}") ### 上传遮罩 + + `subfolder` 参数为 API 兼容性而接受,但在云存储中会被忽略。所有文件都存储在扁平的、内容寻址的命名空间中。 + + ```bash curl curl -X POST "$BASE_URL/api/upload/mask" \ @@ -246,107 +250,6 @@ def upload_mask(file_path: str, original_ref: dict) -> dict: ``` -### 引用已知资源(跳过上传) - -如果您知道某个文件已存在于云存储中(例如,Comfy 共享的热门示例图像),您可以创建资源引用而无需上传任何字节。首先检查哈希是否存在,然后创建您自己的引用。 - - -```bash curl -# 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" - }' -``` - -```typescript TypeScript -async function checkHashExists(hash: string): Promise { - // Note: /api/assets/hash/{hash} only accepts blake3: format - const response = await fetch(`${BASE_URL}/api/assets/hash/${hash}`, { - method: "HEAD", - }); - return response.ok; -} - -async function createAssetFromHash( - hash: string, - name: string, - tags: string[] = ["input"] -): Promise<{ id: string; hash: string }> { - // Note: /api/assets/from-hash accepts both blake3: and sha256: formats - const response = await fetch(`${BASE_URL}/api/assets/from-hash`, { - method: "POST", - headers: getHeaders(), - body: JSON.stringify({ hash, name, tags }), - }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); - return response.json(); -} - -// Use a well-known sample image without uploading -// Hash format: blake3:<64 hex chars> -const sampleHash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd"; - -if (await checkHashExists(sampleHash)) { - const asset = await createAssetFromHash(sampleHash, "sample_portrait.png"); - console.log(`Created asset ${asset.id} pointing to ${asset.hash}`); - - // Use in workflow (assuming workflow is already loaded) - // workflow["1"]["inputs"]["image"] = asset.hash; -} -``` - -```python Python -def check_hash_exists(hash: str) -> bool: - """Check if a file hash exists in cloud storage (unauthenticated). - - Note: This endpoint only accepts blake3: format hashes. - """ - response = requests.head(f"{BASE_URL}/api/assets/hash/{hash}") - return response.status_code == 200 - -def create_asset_from_hash(hash: str, name: str, tags: list = None) -> dict: - """Create an asset reference from an existing hash. - - This skips uploading bytes entirely - useful for well-known files - or files you've previously uploaded to the cloud. - - Note: This endpoint accepts both blake3: and sha256: format hashes. - """ - response = requests.post( - f"{BASE_URL}/api/assets/from-hash", - headers=get_headers(), - json={ - "hash": hash, - "name": name, - "tags": tags or ["input"] - } - ) - response.raise_for_status() - return response.json() - -# Use a well-known sample image without uploading -# Hash format: blake3:<64 hex chars> or sha256:<64 hex chars> -sample_hash = "blake3:a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd" - -if check_hash_exists(sample_hash): - asset = create_asset_from_hash(sample_hash, "sample_portrait.png") - print(f"Created asset {asset['id']} pointing to {asset['hash']}") - - # Use in workflow - workflow["1"]["inputs"]["image"] = asset["hash"] -``` - - --- ## 运行工作流 @@ -1076,13 +979,19 @@ async function downloadOutputs( 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: getHeaders(), + headers: { "X-API-Key": API_KEY }, + redirect: "manual", }); - if (!response.ok) throw new Error(`HTTP ${response.status}`); + 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 response.arrayBuffer())); + await writeFile(path, Buffer.from(await fileResponse.arrayBuffer())); console.log(`Downloaded: ${path}`); } } From a2796f5bee53166aac65ec6f4fcc4b8dad13344f Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:13:36 -0500 Subject: [PATCH 22/27] Add notification message type to WebSocket docs Tested WebSocket real-time updates - all message types work correctly: - status, notification, execution_start, executing, progress, progress_state, executed, execution_success - Binary preview images received during sampling Added missing 'notification' message type which provides user-friendly status messages like 'Executing workflow...' and 'Done!' --- development/cloud/api-reference.mdx | 1 + zh-CN/development/cloud/api-reference.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index f4a79f246..292eb089e 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -707,6 +707,7 @@ 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) | diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index fe04b9367..81610926a 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -707,6 +707,7 @@ outputs = asyncio.run(run_with_websocket()) | 类型 | 描述 | |------|------| | `status` | 队列状态更新,包含 `queue_remaining` 计数 | +| `notification` | 用户友好的状态消息(`value` 字段包含如 "Executing workflow..." 的文本) | | `execution_start` | 工作流执行已开始 | | `executing` | 特定节点正在执行(节点 ID 在 `node` 字段中) | | `progress` | 节点内的步骤进度(采样步骤的 `value`/`max`) | From 2f17f6f99f8d0c41d85cb1ec1563eb2559a6501f Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:32:14 -0500 Subject: [PATCH 23/27] refactor: simplify save_outputs to loop over output types --- development/cloud/api-reference.mdx | 25 +++++------------------ zh-CN/development/cloud/api-reference.mdx | 25 +++++------------------ 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 292eb089e..9b26c7e04 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -867,26 +867,11 @@ def save_outputs(outputs: dict, output_dir: str = "."): os.makedirs(output_dir, exist_ok=True) for node_id, node_outputs in outputs.items(): - # Handle image outputs - if "images" in node_outputs: - for img_info in node_outputs["images"]: - filename = img_info["filename"] - subfolder = img_info.get("subfolder", "") - output_type = img_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}") - - # Handle video outputs - if "video" in node_outputs: - for vid_info in node_outputs["video"]: - filename = vid_info["filename"] - subfolder = vid_info.get("subfolder", "") - output_type = vid_info.get("type", "output") + 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) diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 81610926a..3fb26416d 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -867,26 +867,11 @@ def save_outputs(outputs: dict, output_dir: str = "."): os.makedirs(output_dir, exist_ok=True) for node_id, node_outputs in outputs.items(): - # Handle image outputs - if "images" in node_outputs: - for img_info in node_outputs["images"]: - filename = img_info["filename"] - subfolder = img_info.get("subfolder", "") - output_type = img_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}") - - # Handle video outputs - if "video" in node_outputs: - for vid_info in node_outputs["video"]: - filename = vid_info["filename"] - subfolder = vid_info.get("subfolder", "") - output_type = vid_info.get("type", "output") + 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) From 62b346e5fa678bd30efe0ddff50cc8840b58fc4a Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:53:15 -0500 Subject: [PATCH 24/27] docs: use /api/job/{id}/status for polling, check for success status --- CLOUD_API_RECOMMENDATIONS.md | 6 +- development/cloud/api-reference.mdx | 73 ++++++++--------------- zh-CN/development/cloud/api-reference.mdx | 73 ++++++++--------------- 3 files changed, 50 insertions(+), 102 deletions(-) diff --git a/CLOUD_API_RECOMMENDATIONS.md b/CLOUD_API_RECOMMENDATIONS.md index 74930bc1d..59a38c66a 100644 --- a/CLOUD_API_RECOMMENDATIONS.md +++ b/CLOUD_API_RECOMMENDATIONS.md @@ -26,7 +26,7 @@ The latter would make both endpoints consistent, but would be a breaking change --- -## 2. Inconsistent Status Values Between Endpoints +## 2. Different Status Values Between Endpoints | Endpoint | Status for successful job | Includes outputs? | |----------|---------------------------|-------------------| @@ -37,9 +37,7 @@ The latter would make both endpoints consistent, but would be a breaking change - `/api/jobs/{id}` uses `toFilterStatus()` which maps `StateSuccess` → `completed` - `/api/job/{id}/status` returns raw `jobEntity.Status` directly -This is confusing for API consumers who might expect consistent status values. - -**Recommendation:** Document this clearly (done in API docs) and consider whether `/api/job/{id}/status` should be deprecated in favor of `/api/jobs/{id}`. +The API docs use `/api/job/{id}/status` for polling (lighter weight) and check for `success` status. --- diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 9b26c7e04..648b9d688 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -446,30 +446,21 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") ## Checking Job Status -Poll for job completion and retrieve outputs. - - - The API provides two endpoints for checking job status: - - **`GET /api/jobs/{id}`** - Returns user-friendly status (`pending`, `in_progress`, `completed`, `failed`, `cancelled`) along with outputs. **Recommended for polling.** - - **`GET /api/job/{id}/status`** - Returns raw internal states (e.g., `success`, `executing`, `queued_limited`). Useful for debugging but not recommended for polling logic. - +Poll for job completion. ```bash curl -# Get full job details including outputs -curl -X GET "$BASE_URL/api/jobs/{prompt_id}" \ +curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ``` ```typescript TypeScript -interface JobResponse { - id: string; +interface JobStatus { status: string; - outputs?: Record; } -async function getJob(promptId: string): Promise { - const response = await fetch(`${BASE_URL}/api/jobs/${promptId}`, { +async function getJobStatus(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/job/${promptId}/status`, { headers: getHeaders(), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -480,16 +471,16 @@ async function pollForCompletion( promptId: string, timeout: number = 300, pollInterval: number = 2000 -): Promise { +): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout * 1000) { - const job = await getJob(promptId); + const { status } = await getJobStatus(promptId); - if (job.status === "completed") { - return job; - } else if (["error", "failed", "cancelled"].includes(job.status)) { - throw new Error(`Job failed with status: ${job.status}`); + if (status === "success") { + return; + } else if (["error", "failed", "cancelled"].includes(status)) { + throw new Error(`Job failed with status: ${status}`); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); @@ -498,54 +489,38 @@ async function pollForCompletion( throw new Error(`Job ${promptId} did not complete within ${timeout}s`); } -const result = await pollForCompletion(promptId); -console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); +await pollForCompletion(promptId); +console.log("Job completed!"); ``` ```python Python -def get_job(prompt_id: str) -> dict: - """Get full job details including outputs. - - Returns: - Job data with fields: id, status, outputs - """ +def get_job_status(prompt_id: str) -> str: + """Get the current status of a job.""" response = requests.get( - f"{BASE_URL}/api/jobs/{prompt_id}", + f"{BASE_URL}/api/job/{prompt_id}/status", headers=get_headers() ) response.raise_for_status() - return response.json() + return response.json()["status"] -def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> dict: - """Poll until job completes or times out. - - Args: - prompt_id: The job ID - timeout: Maximum seconds to wait - poll_interval: Seconds between polls - - Returns: - Final job data with outputs - """ +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: - job = get_job(prompt_id) - status = job.get("status") + status = get_job_status(prompt_id) - if status == "completed": - return job + if status == "success": + return elif status in ("error", "failed", "cancelled"): raise RuntimeError(f"Job failed with status: {status}") - # Still pending or in_progress time.sleep(poll_interval) raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s") -# Wait for job to complete -result = poll_for_completion(prompt_id) -print(f"Job completed! Outputs: {list(result.get('outputs', {}).keys())}") +poll_for_completion(prompt_id) +print("Job completed!") ``` diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 3fb26416d..ecaacea8f 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -446,30 +446,21 @@ workflow = set_workflow_input(workflow, "6", "text", "a beautiful landscape") ## 检查任务状态 -轮询任务完成状态并获取输出。 - - - API 提供两个端点用于检查任务状态: - - **`GET /api/jobs/{id}`** - 返回用户友好的状态(`pending`、`in_progress`、`completed`、`failed`、`cancelled`)以及输出。**推荐用于轮询。** - - **`GET /api/job/{id}/status`** - 返回原始内部状态(如 `success`、`executing`、`queued_limited`)。适用于调试,但不建议用于轮询逻辑。 - +轮询任务完成状态。 ```bash curl -# 获取完整任务详情(包括输出) -curl -X GET "$BASE_URL/api/jobs/{prompt_id}" \ +curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \ -H "X-API-Key: $COMFY_CLOUD_API_KEY" ``` ```typescript TypeScript -interface JobResponse { - id: string; +interface JobStatus { status: string; - outputs?: Record; } -async function getJob(promptId: string): Promise { - const response = await fetch(`${BASE_URL}/api/jobs/${promptId}`, { +async function getJobStatus(promptId: string): Promise { + const response = await fetch(`${BASE_URL}/api/job/${promptId}/status`, { headers: getHeaders(), }); if (!response.ok) throw new Error(`HTTP ${response.status}`); @@ -480,16 +471,16 @@ async function pollForCompletion( promptId: string, timeout: number = 300, pollInterval: number = 2000 -): Promise { +): Promise { const startTime = Date.now(); while (Date.now() - startTime < timeout * 1000) { - const job = await getJob(promptId); + const { status } = await getJobStatus(promptId); - if (job.status === "completed") { - return job; - } else if (["error", "failed", "cancelled"].includes(job.status)) { - throw new Error(`Job failed with status: ${job.status}`); + if (status === "success") { + return; + } else if (["error", "failed", "cancelled"].includes(status)) { + throw new Error(`Job failed with status: ${status}`); } await new Promise((resolve) => setTimeout(resolve, pollInterval)); @@ -498,54 +489,38 @@ async function pollForCompletion( throw new Error(`Job ${promptId} did not complete within ${timeout}s`); } -const result = await pollForCompletion(promptId); -console.log(`Job completed! Outputs: ${Object.keys(result.outputs ?? {})}`); +await pollForCompletion(promptId); +console.log("Job completed!"); ``` ```python Python -def get_job(prompt_id: str) -> dict: - """Get full job details including outputs. - - Returns: - Job data with fields: id, status, outputs - """ +def get_job_status(prompt_id: str) -> str: + """Get the current status of a job.""" response = requests.get( - f"{BASE_URL}/api/jobs/{prompt_id}", + f"{BASE_URL}/api/job/{prompt_id}/status", headers=get_headers() ) response.raise_for_status() - return response.json() + return response.json()["status"] -def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> dict: - """Poll until job completes or times out. - - Args: - prompt_id: The job ID - timeout: Maximum seconds to wait - poll_interval: Seconds between polls - - Returns: - Final job data with outputs - """ +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: - job = get_job(prompt_id) - status = job.get("status") + status = get_job_status(prompt_id) - if status == "completed": - return job + if status == "success": + return elif status in ("error", "failed", "cancelled"): raise RuntimeError(f"Job failed with status: {status}") - # Still pending or in_progress time.sleep(poll_interval) raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s") -# Wait for job to complete -result = poll_for_completion(prompt_id) -print(f"Job completed! Outputs: {list(result.get('outputs', {}).keys())}") +poll_for_completion(prompt_id) +print("Job completed!") ``` From 12b13ea8aaf383a2e634255241e1be52eb8cb8c8 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:55:36 -0500 Subject: [PATCH 25/27] docs: separate HTTP errors from WebSocket execution errors --- development/cloud/api-reference.mdx | 67 ++++++----------------- zh-CN/development/cloud/api-reference.mdx | 67 ++++++----------------- 2 files changed, 32 insertions(+), 102 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 648b9d688..01ffb4abe 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -1215,7 +1215,21 @@ def interrupt(): ## Error Handling -The API returns structured errors via the `execution_error` WebSocket message or HTTP error responses. The `exception_type` field identifies the error category: +### 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 | |----------------|-------------| @@ -1223,54 +1237,5 @@ The API returns structured errors via the `execution_error` WebSocket message or | `ModelDownloadError` | Required model not available or failed to download | | `ImageDownloadError` | Failed to download input image from URL | | `OOMError` | Out of GPU memory | -| `PanicError` | Unexpected server crash during execution | -| `ServiceError` | Internal service error | -| `WebSocketError` | WebSocket connection or communication error | -| `DispatcherError` | Job dispatch or routing error | -| `InsufficientFundsError` | Account balance too low | +| `InsufficientFundsError` | Account balance too low (for Partner Nodes) | | `InactiveSubscriptionError` | Subscription not active | - - -```typescript TypeScript -async function handleApiError(response: Response): Promise { - 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); -} -``` - -```python Python -def handle_api_error(response): - """Handle API error responses.""" - if response.status_code == 402: - raise ValueError("Insufficient credits. Please add funds to your account.") - elif response.status_code == 429: - raise ValueError("Rate limited or subscription inactive.") - elif response.status_code >= 400: - try: - error = response.json() - raise ValueError(f"API error: {error.get('message', response.text)}") - except json.JSONDecodeError: - raise ValueError(f"API error: {response.text}") -``` - diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index ecaacea8f..7b7daed66 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -1215,7 +1215,21 @@ def interrupt(): ## 错误处理 -API 通过 `execution_error` WebSocket 消息或 HTTP 错误响应返回结构化错误。`exception_type` 字段标识错误类别: +### HTTP 错误 + +REST API 端点返回标准 HTTP 状态码: + +| 状态码 | 描述 | +|--------|------| +| `400` | 无效请求(错误的工作流、缺少字段) | +| `401` | 未授权(无效或缺少 API 密钥) | +| `402` | 余额不足 | +| `429` | 订阅未激活 | +| `500` | 内部服务器错误 | + +### 执行错误 + +在工作流执行期间,错误通过 `execution_error` WebSocket 消息传递。`exception_type` 字段标识错误类别: | 异常类型 | 描述 | |----------|------| @@ -1223,54 +1237,5 @@ API 通过 `execution_error` WebSocket 消息或 HTTP 错误响应返回结构 | `ModelDownloadError` | 所需模型不可用或下载失败 | | `ImageDownloadError` | 从 URL 下载输入图像失败 | | `OOMError` | GPU 内存不足 | -| `PanicError` | 执行期间发生意外服务器崩溃 | -| `ServiceError` | 内部服务错误 | -| `WebSocketError` | WebSocket 连接或通信错误 | -| `DispatcherError` | 任务分发或路由错误 | -| `InsufficientFundsError` | 账户余额不足 | +| `InsufficientFundsError` | 账户余额不足(用于合作伙伴节点) | | `InactiveSubscriptionError` | 订阅未激活 | - - -```typescript TypeScript -async function handleApiError(response: Response): Promise { - 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); -} -``` - -```python Python -def handle_api_error(response): - """Handle API error responses.""" - if response.status_code == 402: - raise ValueError("Insufficient credits. Please add funds to your account.") - elif response.status_code == 429: - raise ValueError("Rate limited or subscription inactive.") - elif response.status_code >= 400: - try: - error = response.json() - raise ValueError(f"API error: {error.get('message', response.text)}") - except json.JSONDecodeError: - raise ValueError(f"API error: {response.text}") -``` - From a30c06366b8a6b2b54842459dc04d04208e66e2d Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 12:59:54 -0500 Subject: [PATCH 26/27] docs: note that clientId is currently ignored but recommended for forward compatibility --- development/cloud/api-reference.mdx | 4 ++++ zh-CN/development/cloud/api-reference.mdx | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index 01ffb4abe..f6e9c0220 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -530,6 +530,10 @@ print("Job completed!") 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. + + ```typescript TypeScript async function listenForCompletion( diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 7b7daed66..56967c6c9 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -530,6 +530,10 @@ print("Job completed!") 连接 WebSocket 以获取实时执行更新。 + + `clientId` 参数目前会被忽略——同一用户的所有连接都会收到相同的消息。为了向前兼容,仍建议传递唯一的 `clientId`。 + + ```typescript TypeScript async function listenForCompletion( From 1323f0cb10ed699ef147357c727698e345f20e36 Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 6 Jan 2026 13:10:23 -0500 Subject: [PATCH 27/27] docs: note that some endpoints have different semantics for local compatibility --- development/cloud/api-reference.mdx | 2 +- zh-CN/development/cloud/api-reference.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/development/cloud/api-reference.mdx b/development/cloud/api-reference.mdx index f6e9c0220..2ee7d5f90 100644 --- a/development/cloud/api-reference.mdx +++ b/development/cloud/api-reference.mdx @@ -4,7 +4,7 @@ description: "Complete API reference with code examples for Comfy Cloud" --- - **Experimental API:** This API is experimental and subject to change. Endpoints, request/response formats, and behavior may be modified without notice. + **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 diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx index 56967c6c9..daeba0414 100644 --- a/zh-CN/development/cloud/api-reference.mdx +++ b/zh-CN/development/cloud/api-reference.mdx @@ -4,7 +4,7 @@ description: "Comfy Cloud 的完整 API 参考及代码示例" --- - **实验性 API:** 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。 + **实验性 API:** 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。部分端点为兼容本地 ComfyUI 而保留,但可能具有不同的语义(例如,某些字段会被忽略)。 # Cloud API 参考