diff --git a/CLOUD_API_RECOMMENDATIONS.md b/CLOUD_API_RECOMMENDATIONS.md
new file mode 100644
index 00000000..59a38c66
--- /dev/null
+++ b/CLOUD_API_RECOMMENDATIONS.md
@@ -0,0 +1,116 @@
+# 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. Different 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
+
+The API docs use `/api/job/{id}/status` for polling (lighter weight) and check for `success` status.
+
+---
+
+## 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
diff --git a/api-reference/cloud/overview.mdx b/api-reference/cloud/overview.mdx
new file mode 100644
index 00000000..11fbc8d1
--- /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/api-reference.mdx b/development/cloud/api-reference.mdx
new file mode 100644
index 00000000..2ee7d5f9
--- /dev/null
+++ b/development/cloud/api-reference.mdx
@@ -0,0 +1,1245 @@
+---
+title: "Cloud API Reference"
+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. Some endpoints are maintained for compatibility with local ComfyUI but may have different semantics (e.g., ignored fields).
+
+
+# Cloud API Reference
+
+This page provides complete examples for common Comfy Cloud API operations.
+
+
+ **Subscription Required:** Running workflows via the API requires an active Comfy Cloud subscription. See [pricing plans](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api) 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", {})
+inputs = list(ksampler.get('input', {}).get('required', {}).keys())
+print(f"KSampler inputs: {inputs}")
+```
+
+
+---
+
+## 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
+
+
+ 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" \
+ -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()
+```
+
+
+---
+
+## 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}")
+```
+
+
+### 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 submitWorkflowWithPartnerNodes(
+ workflow: Record,
+ apiKey: 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: apiKey,
+ },
+ }),
+ });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const result = await response.json();
+ return result.prompt_id;
+}
+
+// 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_partner_nodes(workflow: dict, api_key: str) -> str:
+ """Submit a workflow that uses Partner Nodes.
+
+ Args:
+ workflow: ComfyUI workflow in API format
+ api_key: Your 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": api_key
+ }
+ }
+ )
+ response.raise_for_status()
+ return response.json()["prompt_id"]
+
+# Use when workflow contains Partner Nodes
+prompt_id = submit_workflow_with_partner_nodes(workflow, API_KEY)
+```
+
+
+
+ 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
+
+
+```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.
+
+
+```bash curl
+curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \
+ -H "X-API-Key: $COMFY_CLOUD_API_KEY"
+```
+
+```typescript TypeScript
+interface JobStatus {
+ status: string;
+}
+
+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 === "success") {
+ return;
+ } else if (["error", "failed", "cancelled"].includes(status)) {
+ throw new Error(`Job failed with status: ${status}`);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
+ }
+
+ throw new Error(`Job ${promptId} did not complete within ${timeout}s`);
+}
+
+await pollForCompletion(promptId);
+console.log("Job completed!");
+```
+
+```python Python
+def get_job_status(prompt_id: str) -> str:
+ """Get the current status of a job."""
+ response = requests.get(
+ f"{BASE_URL}/api/job/{prompt_id}/status",
+ headers=get_headers()
+ )
+ response.raise_for_status()
+ return response.json()["status"]
+
+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:
+ status = get_job_status(prompt_id)
+
+ if status == "success":
+ return
+ elif status in ("error", "failed", "cancelled"):
+ raise RuntimeError(f"Job failed with status: {status}")
+
+ time.sleep(poll_interval)
+
+ raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s")
+
+poll_for_completion(prompt_id)
+print("Job completed!")
+```
+
+
+---
+
+## WebSocket for Real-Time Progress
+
+Connect to the WebSocket for real-time execution updates.
+
+
+ The `clientId` parameter is currently ignored—all connections for a user receive the same messages. Pass a unique `clientId` for forward compatibility.
+
+
+
+```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 |
+| `notification` | User-friendly status message (`value` field contains text like "Executing workflow...") |
+| `execution_start` | Workflow execution has started |
+| `executing` | A specific node is now executing (node ID in `node` field) |
+| `progress` | Step progress within a node (`value`/`max` for sampling steps) |
+| `progress_state` | Extended progress state with node metadata (nested `nodes` object) |
+| `executed` | Node completed with outputs (images, video, etc. in `output` field) |
+| `execution_cached` | Nodes skipped because outputs are cached (`nodes` array) |
+| `execution_success` | Entire workflow completed successfully |
+| `execution_error` | Workflow failed (includes `exception_type`, `exception_message`, `traceback`) |
+| `execution_interrupted` | Workflow was cancelled by user |
+
+#### Binary Messages (Preview Images)
+
+During image generation, ComfyUI sends **binary WebSocket frames** containing preview images. These are raw binary data (not JSON):
+
+| Binary Type | Value | Description |
+|-------------|-------|-------------|
+| `PREVIEW_IMAGE` | `1` | In-progress preview during diffusion sampling |
+| `TEXT` | `3` | Text output from nodes (progress text) |
+| `PREVIEW_IMAGE_WITH_METADATA` | `4` | Preview image with node context metadata |
+
+**Binary frame formats** (all integers are big-endian):
+
+
+
+ | Offset | Size | Field | Description |
+ |--------|------|-------|-------------|
+ | 0 | 4 bytes | `type` | `0x00000001` |
+ | 4 | 4 bytes | `image_type` | Format code (1=JPEG, 2=PNG) |
+ | 8 | variable | `image_data` | Raw image bytes |
+
+
+
+ | Offset | Size | Field | Description |
+ |--------|------|-------|-------------|
+ | 0 | 4 bytes | `type` | `0x00000003` |
+ | 4 | 4 bytes | `node_id_len` | Length of node_id string |
+ | 8 | N bytes | `node_id` | UTF-8 encoded node ID |
+ | 8+N | variable | `text` | UTF-8 encoded progress text |
+
+
+
+ | Offset | Size | Field | Description |
+ |--------|------|-------|-------------|
+ | 0 | 4 bytes | `type` | `0x00000004` |
+ | 4 | 4 bytes | `metadata_len` | Length of metadata JSON |
+ | 8 | N bytes | `metadata` | UTF-8 JSON (see below) |
+ | 8+N | variable | `image_data` | Raw JPEG/PNG bytes |
+
+ **Metadata JSON structure:**
+ ```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 (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
+```
+
+```typescript TypeScript
+async function downloadOutput(
+ filename: string,
+ subfolder: string = "",
+ 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: { "X-API-Key": API_KEY },
+ redirect: "manual",
+ });
+ if (response.status !== 302) throw new Error(`HTTP ${response.status}`);
+ const signedUrl = response.headers.get("location")!;
+ // Fetch from signed URL without auth headers
+ const fileResponse = await fetch(signedUrl);
+ if (!fileResponse.ok) throw new Error(`HTTP ${fileResponse.status}`);
+ return fileResponse.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():
+ 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)
+
+ 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",
+ });
+ // Get redirect URL (don't follow to avoid sending auth to storage)
+ const response = await fetch(`${BASE_URL}/api/view?${params}`, {
+ headers: { "X-API-Key": API_KEY },
+ redirect: "manual",
+ });
+ if (response.status !== 302) throw new Error(`HTTP ${response.status}`);
+ const signedUrl = response.headers.get("location")!;
+ // Fetch from signed URL without auth headers
+ const fileResponse = await fetch(signedUrl);
+ if (!fileResponse.ok) throw new Error(`HTTP ${fileResponse.status}`);
+
+ const path = `${outputDir}/${fileInfo.filename}`;
+ await writeFile(path, Buffer.from(await fileResponse.arrayBuffer()));
+ console.log(`Downloaded: ${path}`);
+ }
+ }
+ }
+}
+
+async function main() {
+ // 1. Load workflow
+ const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8"));
+
+ // 2. Modify workflow parameters
+ workflow["3"].inputs.seed = 42;
+ workflow["6"].inputs.text = "a beautiful sunset over mountains";
+
+ // 3. Submit workflow
+ const promptId = await submitWorkflow(workflow);
+ console.log(`Job submitted: ${promptId}`);
+
+ // 4. Wait for completion with progress
+ const outputs = await waitForCompletion(promptId);
+ console.log(`Job completed! Found ${Object.keys(outputs).length} output nodes`);
+
+ // 5. Download outputs
+ await downloadOutputs(outputs, "./outputs");
+ console.log("Done!");
+}
+
+main();
+```
+
+```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
+
+### HTTP Errors
+
+REST API endpoints return standard HTTP status codes:
+
+| Status | Description |
+|--------|-------------|
+| `400` | Invalid request (bad workflow, missing fields) |
+| `401` | Unauthorized (invalid or missing API key) |
+| `402` | Insufficient credits |
+| `429` | Subscription inactive |
+| `500` | Internal server error |
+
+### Execution Errors
+
+During workflow execution, errors are delivered via the `execution_error` WebSocket message. The `exception_type` field identifies the error category:
+
+| Exception Type | Description |
+|----------------|-------------|
+| `ValidationError` | Invalid workflow or inputs |
+| `ModelDownloadError` | Required model not available or failed to download |
+| `ImageDownloadError` | Failed to download input image from URL |
+| `OOMError` | Out of GPU memory |
+| `InsufficientFundsError` | Account balance too low (for Partner Nodes) |
+| `InactiveSubscriptionError` | Subscription not active |
diff --git a/development/cloud/openapi.mdx b/development/cloud/openapi.mdx
new file mode 100644
index 00000000..82177433
--- /dev/null
+++ b/development/cloud/openapi.mdx
@@ -0,0 +1,72 @@
+---
+title: "OpenAPI Specification"
+description: "Machine-readable OpenAPI specification for Comfy 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.
+
+
+# Comfy Cloud API Specification
+
+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&utm_campaign=cloud-api) 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 00000000..aabf9bd5
--- /dev/null
+++ b/development/cloud/overview.mdx
@@ -0,0 +1,177 @@
+---
+title: "Cloud API Overview"
+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.
+
+
+# Comfy Cloud API
+
+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&utm_campaign=cloud-api) 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
+ 
+
+
+ Click `+ New` in API Keys to create an API Key
+ 
+
+
+
+ 
+ 1. (Required) Enter the API Key name,
+ 2. Click `Generate` to create
+
+
+ 
+
+ 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, which returns a 302 redirect to a temporary signed URL.
+
+## 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 640d2c26..b04abf07 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 3c7e7d0e..e7e9975e 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"
+ }
}
]
},
@@ -1208,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": [
@@ -1268,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"
@@ -1353,6 +1379,13 @@
{
"tab": "Registry API 参考文档",
"openapi": "https://api.comfy.org/openapi"
+ },
+ {
+ "tab": "Cloud API 参考文档",
+ "openapi": {
+ "source": "openapi-cloud.yaml",
+ "directory": "zh-CN/api-reference/cloud"
+ }
}
]
}
diff --git a/get_started/cloud.mdx b/get_started/cloud.mdx
index 1b5a303e..37ec642a 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 c526dc49..1f68bc0c 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 00000000..f00c797e
--- /dev/null
+++ b/openapi-cloud.yaml
@@ -0,0 +1,3709 @@
+openapi: 3.0.3
+info:
+ 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 Comfy Cloud - Run ComfyUI workflows on cloud infrastructure.
+
+ 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
+
+ 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 |
+ |-------|-----------|----------------|
+ | `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: Comfy 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:
+ $ref: '#/components/schemas/QueueItem'
+ queue_pending:
+ type: array
+ description: Array of pending job items (ordered by creation time, oldest first)
+ items:
+ $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
+ 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
diff --git a/zh-CN/api-reference/cloud/overview.mdx b/zh-CN/api-reference/cloud/overview.mdx
new file mode 100644
index 00000000..2fa5d720
--- /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 模型 |
+| 节点 | 获取可用节点的信息 |
+| 用户 | 账户信息和个人数据 |
+| 系统 | 服务器状态和健康检查 |
diff --git a/zh-CN/development/cloud/api-reference.mdx b/zh-CN/development/cloud/api-reference.mdx
new file mode 100644
index 00000000..daeba041
--- /dev/null
+++ b/zh-CN/development/cloud/api-reference.mdx
@@ -0,0 +1,1245 @@
+---
+title: "Cloud API 参考"
+description: "Comfy Cloud 的完整 API 参考及代码示例"
+---
+
+
+ **实验性 API:** 此 API 处于实验阶段,可能会发生变化。端点、请求/响应格式和行为可能会在未事先通知的情况下进行修改。部分端点为兼容本地 ComfyUI 而保留,但可能具有不同的语义(例如,某些字段会被忽略)。
+
+
+# Cloud API 参考
+
+本页面提供了常见 Comfy Cloud API 操作的完整示例。
+
+
+ **需要订阅:** 通过 API 运行工作流需要有效的 Comfy Cloud 订阅。请查看[定价方案](https://www.comfy.org/cloud/pricing?utm_source=docs&utm_campaign=cloud-api)了解详情。
+
+
+## 设置
+
+所有示例都使用以下通用的导入和配置:
+
+
+```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", {})
+inputs = list(ksampler.get('input', {}).get('required', {}).keys())
+print(f"KSampler inputs: {inputs}")
+```
+
+
+---
+
+## 上传输入
+
+上传图像、遮罩或其他文件以在工作流中使用。
+
+### 直接上传(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']}")
+```
+
+
+### 上传遮罩
+
+
+ `subfolder` 参数为 API 兼容性而接受,但在云存储中会被忽略。所有文件都存储在扁平的、内容寻址的命名空间中。
+
+
+
+```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()
+```
+
+
+---
+
+## 运行工作流
+
+提交工作流以执行。
+
+### 提交工作流
+
+
+```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}")
+```
+
+
+### 使用合作伙伴节点
+
+如果您的工作流包含[合作伙伴节点](/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 submitWorkflowWithPartnerNodes(
+ workflow: Record,
+ apiKey: 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: apiKey,
+ },
+ }),
+ });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ const result = await response.json();
+ return result.prompt_id;
+}
+
+// 当工作流包含合作伙伴节点时使用(例如 Flux Pro、Ideogram 等)
+const promptId = await submitWorkflowWithPartnerNodes(workflow, API_KEY);
+```
+
+```python Python
+def submit_workflow_with_partner_nodes(workflow: dict, api_key: str) -> str:
+ """提交使用合作伙伴节点的工作流。
+
+ Args:
+ workflow: API 格式的 ComfyUI 工作流
+ api_key: 来自 platform.comfy.org 的 API 密钥
+
+ Returns:
+ 用于跟踪任务的 prompt_id
+ """
+ response = requests.post(
+ f"{BASE_URL}/api/prompt",
+ headers=get_headers(),
+ json={
+ "prompt": workflow,
+ "extra_data": {
+ "api_key_comfy_org": api_key
+ }
+ }
+ )
+ response.raise_for_status()
+ return response.json()["prompt_id"]
+
+# 当工作流包含合作伙伴节点时使用
+prompt_id = submit_workflow_with_partner_nodes(workflow, API_KEY)
+```
+
+
+
+ 在 [platform.comfy.org](https://platform.comfy.org/login) 生成您的 API 密钥。此密钥与 Cloud API 身份验证(`X-API-Key` 请求头)使用的是同一个密钥。
+
+
+### 修改工作流输入
+
+
+```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
+curl -X GET "$BASE_URL/api/job/{prompt_id}/status" \
+ -H "X-API-Key: $COMFY_CLOUD_API_KEY"
+```
+
+```typescript TypeScript
+interface JobStatus {
+ status: string;
+}
+
+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 === "success") {
+ return;
+ } else if (["error", "failed", "cancelled"].includes(status)) {
+ throw new Error(`Job failed with status: ${status}`);
+ }
+
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
+ }
+
+ throw new Error(`Job ${promptId} did not complete within ${timeout}s`);
+}
+
+await pollForCompletion(promptId);
+console.log("Job completed!");
+```
+
+```python Python
+def get_job_status(prompt_id: str) -> str:
+ """Get the current status of a job."""
+ response = requests.get(
+ f"{BASE_URL}/api/job/{prompt_id}/status",
+ headers=get_headers()
+ )
+ response.raise_for_status()
+ return response.json()["status"]
+
+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:
+ status = get_job_status(prompt_id)
+
+ if status == "success":
+ return
+ elif status in ("error", "failed", "cancelled"):
+ raise RuntimeError(f"Job failed with status: {status}")
+
+ time.sleep(poll_interval)
+
+ raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s")
+
+poll_for_completion(prompt_id)
+print("Job completed!")
+```
+
+
+---
+
+## 实时进度 WebSocket
+
+连接 WebSocket 以获取实时执行更新。
+
+
+ `clientId` 参数目前会被忽略——同一用户的所有连接都会收到相同的消息。为了向前兼容,仍建议传递唯一的 `clientId`。
+
+
+
+```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` 计数 |
+| `notification` | 用户友好的状态消息(`value` 字段包含如 "Executing workflow..." 的文本) |
+| `execution_start` | 工作流执行已开始 |
+| `executing` | 特定节点正在执行(节点 ID 在 `node` 字段中) |
+| `progress` | 节点内的步骤进度(采样步骤的 `value`/`max`) |
+| `progress_state` | 扩展进度状态,包含节点元数据(嵌套的 `nodes` 对象) |
+| `executed` | 节点完成并输出结果(图像、视频等在 `output` 字段中) |
+| `execution_cached` | 因输出已缓存而跳过的节点(`nodes` 数组) |
+| `execution_success` | 整个工作流成功完成 |
+| `execution_error` | 工作流失败(包含 `exception_type`、`exception_message`、`traceback`) |
+| `execution_interrupted` | 工作流被用户取消 |
+
+#### 二进制消息(预览图像)
+
+在图像生成过程中,ComfyUI 会发送包含预览图像的**二进制 WebSocket 帧**。这些是原始二进制数据(不是 JSON):
+
+| 二进制类型 | 值 | 描述 |
+|------------|-----|------|
+| `PREVIEW_IMAGE` | `1` | 扩散采样期间的进度预览 |
+| `TEXT` | `3` | 节点的文本输出(进度文本) |
+| `PREVIEW_IMAGE_WITH_METADATA` | `4` | 带有节点上下文元数据的预览图像 |
+
+**二进制帧格式**(所有整数为大端序):
+
+
+
+ | 偏移 | 大小 | 字段 | 描述 |
+ |------|------|------|------|
+ | 0 | 4 字节 | `type` | `0x00000001` |
+ | 4 | 4 字节 | `image_type` | 格式代码(1=JPEG, 2=PNG) |
+ | 8 | 可变 | `image_data` | 原始图像字节 |
+
+
+
+ | 偏移 | 大小 | 字段 | 描述 |
+ |------|------|------|------|
+ | 0 | 4 字节 | `type` | `0x00000003` |
+ | 4 | 4 字节 | `node_id_len` | node_id 字符串的长度 |
+ | 8 | N 字节 | `node_id` | UTF-8 编码的节点 ID |
+ | 8+N | 可变 | `text` | UTF-8 编码的进度文本 |
+
+
+
+ | 偏移 | 大小 | 字段 | 描述 |
+ |------|------|------|------|
+ | 0 | 4 字节 | `type` | `0x00000004` |
+ | 4 | 4 字节 | `metadata_len` | 元数据 JSON 的长度 |
+ | 8 | N 字节 | `metadata` | UTF-8 JSON(见下文) |
+ | 8+N | 可变 | `image_data` | 原始 JPEG/PNG 字节 |
+
+ **元数据 JSON 结构:**
+ ```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
+# 下载单个输出文件(使用 -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
+```
+
+```typescript TypeScript
+async function downloadOutput(
+ filename: string,
+ subfolder: string = "",
+ 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: { "X-API-Key": API_KEY },
+ redirect: "manual",
+ });
+ if (response.status !== 302) throw new Error(`HTTP ${response.status}`);
+ const signedUrl = response.headers.get("location")!;
+ // Fetch from signed URL without auth headers
+ const fileResponse = await fetch(signedUrl);
+ if (!fileResponse.ok) throw new Error(`HTTP ${fileResponse.status}`);
+ return fileResponse.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():
+ 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)
+
+ 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",
+ });
+ // Get redirect URL (don't follow to avoid sending auth to storage)
+ const response = await fetch(`${BASE_URL}/api/view?${params}`, {
+ headers: { "X-API-Key": API_KEY },
+ redirect: "manual",
+ });
+ if (response.status !== 302) throw new Error(`HTTP ${response.status}`);
+ const signedUrl = response.headers.get("location")!;
+ // Fetch from signed URL without auth headers
+ const fileResponse = await fetch(signedUrl);
+ if (!fileResponse.ok) throw new Error(`HTTP ${fileResponse.status}`);
+
+ const path = `${outputDir}/${fileInfo.filename}`;
+ await writeFile(path, Buffer.from(await fileResponse.arrayBuffer()));
+ console.log(`Downloaded: ${path}`);
+ }
+ }
+ }
+}
+
+async function main() {
+ // 1. Load workflow
+ const workflow = JSON.parse(await readFile("workflow_api.json", "utf-8"));
+
+ // 2. Modify workflow parameters
+ workflow["3"].inputs.seed = 42;
+ workflow["6"].inputs.text = "a beautiful sunset over mountains";
+
+ // 3. Submit workflow
+ const promptId = await submitWorkflow(workflow);
+ console.log(`Job submitted: ${promptId}`);
+
+ // 4. Wait for completion with progress
+ const outputs = await waitForCompletion(promptId);
+ console.log(`Job completed! Found ${Object.keys(outputs).length} output nodes`);
+
+ // 5. Download outputs
+ await downloadOutputs(outputs, "./outputs");
+ console.log("Done!");
+}
+
+main();
+```
+
+```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()
+```
+
+
+---
+
+## 错误处理
+
+### HTTP 错误
+
+REST API 端点返回标准 HTTP 状态码:
+
+| 状态码 | 描述 |
+|--------|------|
+| `400` | 无效请求(错误的工作流、缺少字段) |
+| `401` | 未授权(无效或缺少 API 密钥) |
+| `402` | 余额不足 |
+| `429` | 订阅未激活 |
+| `500` | 内部服务器错误 |
+
+### 执行错误
+
+在工作流执行期间,错误通过 `execution_error` WebSocket 消息传递。`exception_type` 字段标识错误类别:
+
+| 异常类型 | 描述 |
+|----------|------|
+| `ValidationError` | 无效的工作流或输入 |
+| `ModelDownloadError` | 所需模型不可用或下载失败 |
+| `ImageDownloadError` | 从 URL 下载输入图像失败 |
+| `OOMError` | GPU 内存不足 |
+| `InsufficientFundsError` | 账户余额不足(用于合作伙伴节点) |
+| `InactiveSubscriptionError` | 订阅未激活 |
diff --git a/zh-CN/development/cloud/openapi.mdx b/zh-CN/development/cloud/openapi.mdx
new file mode 100644
index 00000000..1f9a32ce
--- /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&utm_campaign=cloud-api)。
+
+
+## 交互式 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 00000000..38bc7ffa
--- /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&utm_campaign=cloud-api)。
+
+
+## 基础 URL
+
+```
+https://cloud.comfy.org
+```
+
+## 身份验证
+
+所有 API 请求都需要通过 `X-API-Key` 请求头传递 API 密钥。
+
+### 获取 API 密钥
+
+
+
+ 请访问 https://platform.comfy.org/login 并使用相应账户登录
+ 
+
+
+ 点击 API Keys 中的 `+ New` 创建 API Key
+ 
+
+
+
+ 
+ 1. (必填)输入 API Key 名称,
+ 2. 点击 `Generate` 生成
+
+
+ 
+
+ 由于 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` 端点下载,该端点会返回 302 重定向到临时签名 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#运行工作流) | 提交工作流、检查状态 |
+| [任务](/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#对象信息) | 可用节点及其定义 |
+
+## 后续步骤
+
+- [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 d7b354af..fbda9585 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 93635f74..75297d7a 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 的云端版本,具有与本地版本相同的功能
- 点击工作流我们的服务会开始为你的工作流分配机器,在队列面板中你可以了解对应工作流的执行状态
+ 点击运行后,我们的服务会开始为你的工作流分配机器,你可以在队列面板中查看该工作流的执行状态。

@@ -132,6 +132,10 @@ ComfyUI Cloud 是 ComfyUI 的云端版本,具有与本地版本相同的功能
## 下一步
+
+ 通过 Cloud API 以编程方式运行工作流
+
+
探索教程以学习 ComfyUI 工作流
diff --git a/zh-CN/index.mdx b/zh-CN/index.mdx
index a84bd4e8..11dff271 100644
--- a/zh-CN/index.mdx
+++ b/zh-CN/index.mdx
@@ -132,11 +132,18 @@ sidebarTitle: "介绍"
创建和发布自定义节点
- 将 ComfyUI 集成到你的应用程序
+ 集成本地 ComfyUI 服务器
+
+
+ 通过 Comfy Cloud API 运行工作流