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 + ![Visit Platform Login Page](/images/interface/setting/user/user-login-api-key-1.jpg) + + + Click `+ New` in API Keys to create an API Key + ![Create API Key](/images/interface/setting/user/user-login-api-key-2.jpg) + + + + ![Enter API Key Name](/images/interface/setting/user/user-login-api-key-3.jpg) + 1. (Required) Enter the API Key name, + 2. Click `Generate` to create + + + ![Obtain API Key](/images/interface/setting/user/user-login-api-key-4.jpg) + + Since the API Key is only visible upon first creation, please save it immediately after creation. It cannot be viewed later, so please keep it safe. + Please note that you should not share your API Key with others. Once it leaked, you can delete it and create a new one. + + + + + + Keep your API key secure. Never commit it to version control or share it publicly. + + +### Using the API Key + +Pass your API key in the `X-API-Key` header with every request: + + +```bash curl +curl -X GET "https://cloud.comfy.org/api/user" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +const API_KEY = process.env.COMFY_CLOUD_API_KEY!; + +const response = await fetch("https://cloud.comfy.org/api/user", { + headers: { "X-API-Key": API_KEY }, +}); +const user = await response.json(); +``` + +```python Python +import os +import requests + +API_KEY = os.environ["COMFY_CLOUD_API_KEY"] +headers = {"X-API-Key": API_KEY} + +response = requests.get( + "https://cloud.comfy.org/api/user", + headers=headers +) +``` + + +## Core Concepts + +### Workflows + +ComfyUI workflows are JSON objects describing a graph of nodes. The API accepts workflows in the "API format" (node IDs as keys with class_type, inputs, etc.) as produced by the ComfyUI frontend's "Save (API Format)" option. + +### Jobs + +When you submit a workflow, a **job** is created. Jobs are executed asynchronously: +1. Submit workflow via `POST /api/prompt` +2. Receive a `prompt_id` (job ID) +3. Monitor progress via WebSocket or poll for status +4. Retrieve outputs when complete + +### Outputs + +Generated content (images, videos, audio) is stored in cloud storage. Output files can be downloaded via the `/api/view` endpoint, 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 并使用相应账户登录 + ![访问平台登录页面](/images/interface/setting/user/user-login-api-key-1.jpg) + + + 点击 API Keys 中的 `+ New` 创建 API Key + ![创建 API Key](/images/interface/setting/user/user-login-api-key-2.jpg) + + + + ![输入 API Key 名称](/images/interface/setting/user/user-login-api-key-3.jpg) + 1. (必填)输入 API Key 名称, + 2. 点击 `Generate` 生成 + + + ![获取 API Key](/images/interface/setting/user/user-login-api-key-4.jpg) + + 由于 API Key 仅在首次创建时可见,请在创建后立即保存。之后将无法再次查看,请妥善保管。 + 请注意不要与他人分享您的 API Key。一旦泄露,您可以删除它并创建新的。 + + + + + + 请妥善保管您的 API 密钥。切勿将其提交到版本控制系统或公开分享。 + + +### 使用 API 密钥 + +在每个请求中通过 `X-API-Key` 请求头传递您的 API 密钥: + + +```bash curl +curl -X GET "https://cloud.comfy.org/api/user" \ + -H "X-API-Key: $COMFY_CLOUD_API_KEY" +``` + +```typescript TypeScript +const API_KEY = process.env.COMFY_CLOUD_API_KEY!; + +const response = await fetch("https://cloud.comfy.org/api/user", { + headers: { "X-API-Key": API_KEY }, +}); +const user = await response.json(); +``` + +```python Python +import os +import requests + +API_KEY = os.environ["COMFY_CLOUD_API_KEY"] +headers = {"X-API-Key": API_KEY} + +response = requests.get( + "https://cloud.comfy.org/api/user", + headers=headers +) +``` + + +## 核心概念 + +### 工作流 + +ComfyUI 工作流是描述节点图的 JSON 对象。API 接受"API 格式"的工作流(以节点 ID 为键,包含 class_type、inputs 等),该格式由 ComfyUI 前端的"Save (API Format)"选项导出。 + +### 任务 + +当您提交工作流时,会创建一个**任务**。任务以异步方式执行: +1. 通过 `POST /api/prompt` 提交工作流 +2. 收到 `prompt_id`(任务 ID) +3. 通过 WebSocket 监控进度或轮询状态 +4. 完成后获取输出 + +### 输出 + +生成的内容(图像、视频、音频)存储在云存储中。输出文件可通过 `/api/view` 端点下载,该端点会返回 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 的云端版本,具有与本地版本相同的功能 - 点击工作流我们的服务会开始为你的工作流分配机器,在队列面板中你可以了解对应工作流的执行状态 + 点击运行后,我们的服务会开始为你的工作流分配机器,你可以在队列面板中查看该工作流的执行状态。 ![查看队列](/images/cloud/workflow_check_queue.webp) @@ -132,6 +132,10 @@ ComfyUI Cloud 是 ComfyUI 的云端版本,具有与本地版本相同的功能 ## 下一步 + + 通过 Cloud API 以编程方式运行工作流 + + 探索教程以学习 ComfyUI 工作流 diff --git a/zh-CN/index.mdx b/zh-CN/index.mdx index a84bd4e8..11dff271 100644 --- a/zh-CN/index.mdx +++ b/zh-CN/index.mdx @@ -132,11 +132,18 @@ sidebarTitle: "介绍" 创建和发布自定义节点 - 将 ComfyUI 集成到你的应用程序 + 集成本地 ComfyUI 服务器 + + + 通过 Comfy Cloud API 运行工作流