From 237ef41be07b429f2d97bd76dc840190f7e5b63b Mon Sep 17 00:00:00 2001 From: Hunter Senft-Grupp Date: Tue, 30 Dec 2025 19:03:19 -0500 Subject: [PATCH] PROTOTYPE DO NOT COMMIT --- cloud/sdk/authentication.mdx | 135 +++ cloud/sdk/overview.mdx | 107 ++ cloud/sdk/python.mdx | 275 +++++ cloud/sdk/use-cases.mdx | 301 +++++ cloud/sdk/webhooks.mdx | 245 ++++ cloud/sdk/websockets.mdx | 206 ++++ docs.json | 30 + openapi-developer.yaml | 2034 ++++++++++++++++++++++++++++++++++ 8 files changed, 3333 insertions(+) create mode 100644 cloud/sdk/authentication.mdx create mode 100644 cloud/sdk/overview.mdx create mode 100644 cloud/sdk/python.mdx create mode 100644 cloud/sdk/use-cases.mdx create mode 100644 cloud/sdk/webhooks.mdx create mode 100644 cloud/sdk/websockets.mdx create mode 100644 openapi-developer.yaml diff --git a/cloud/sdk/authentication.mdx b/cloud/sdk/authentication.mdx new file mode 100644 index 00000000..9d923a67 --- /dev/null +++ b/cloud/sdk/authentication.mdx @@ -0,0 +1,135 @@ +--- +title: "Authentication" +description: "Authenticate with the ComfyUI Cloud Developer API" +--- + +## API Keys + +All Developer API requests require authentication via API key. These are the same API keys used for [Partner Nodes](/tutorials/partner-nodes/overview) and other Comfy platform features. + +### Getting Your API Key + +API keys are managed at [platform.comfy.org](https://platform.comfy.org/login): + + + + Go to [platform.comfy.org/login](https://platform.comfy.org/login) and sign in with your Comfy account (Email, Google, or GitHub). + ![Visit Platform Login Page](/images/interface/setting/user/user-login-api-key-1.jpg) + + + Click **+ New** in the API Keys section. + ![Create API Key](/images/interface/setting/user/user-login-api-key-2.jpg) + + + Enter a descriptive name (e.g., "Production Backend", "Local Dev") and click **Generate**. + ![Enter API Key Name](/images/interface/setting/user/user-login-api-key-3.jpg) + + + Copy and save your API key immediately. + ![Obtain API Key](/images/interface/setting/user/user-login-api-key-4.jpg) + + The API key is only shown once at creation. Store it securely - you cannot view it again later. + + + + +### Managing API Keys + +You can view and delete your API keys at [platform.comfy.org](https://platform.comfy.org/login): + +![API Key Management](/images/interface/setting/user/user-login-api-key-5.jpg) + +Delete any unused keys or keys that may have been compromised. + +## Using Your API Key + +Include the API key in the `Authorization` header: + +```bash +curl https://cloud.comfy.org/developer/api/v1/account \ + -H "Authorization: Bearer comfyui-abc123..." +``` + +With the Python SDK: + +```python +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key="comfyui-abc123...") +``` + +### Environment Variables + +We recommend storing your API key in environment variables: + +```bash +export COMFY_API_KEY="comfyui-abc123..." +``` + +```python +import os +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key=os.environ["COMFY_API_KEY"]) +``` + + +Never commit API keys to version control. Use environment variables or a secrets manager. + + +## Environments + +| Environment | Base URL | +|-------------|----------| +| Production | `https://cloud.comfy.org/developer/api/v1` | +| Staging | `https://stagingcloud.comfy.org/developer/api/v1` | + +```python +# Using staging +client = ComfyCloudClient( + api_key="comfyui-...", + base_url="https://stagingcloud.comfy.org/developer/api/v1", +) +``` + +## Rate Limits + +The API has rate limits to ensure fair usage: + +| Limit | Value | +|-------|-------| +| Requests per minute | 600 | +| Concurrent jobs | Based on plan | +| Max upload size | 100 MB | + +When rate limited, you'll receive a `429 Too Many Requests` response. Implement exponential backoff in your retry logic. + +## Error Responses + +Authentication errors return `401 Unauthorized`: + +```json +{ + "error": { + "type": "unauthorized", + "message": "Invalid or expired API key" + } +} +``` + +Common causes: +- Missing `Authorization` header +- Malformed header (should be `Bearer `) +- Revoked or deleted API key +- Using wrong environment's key + +## Related + + + + Full account management and login options + + + Using API keys with Partner Nodes + + diff --git a/cloud/sdk/overview.mdx b/cloud/sdk/overview.mdx new file mode 100644 index 00000000..5f4639ce --- /dev/null +++ b/cloud/sdk/overview.mdx @@ -0,0 +1,107 @@ +--- +title: "Developer API Overview" +description: "Programmatic access to ComfyUI Cloud for building applications and automations" +--- + +The ComfyUI Cloud Developer API provides a clean, SDK-friendly interface for running ComfyUI workflows programmatically. Use it to build applications, batch processing pipelines, or integrate AI image generation into your products. + +## Why Use the Developer API? + + + + Designed for developers, not constrained by ComfyUI frontend compatibility + + + Type-safe clients for Python, JavaScript, and Go + + + WebSocket streaming for live progress updates + + + Push notifications when jobs complete + + + +## Core Concepts + +### Resources + +| Resource | Description | +|----------|-------------| +| **Inputs** | Files you upload for use in workflows (images, masks, etc.) | +| **Models** | Custom models you bring (BYOM - Bring Your Own Model) | +| **Jobs** | Workflow executions | +| **Outputs** | Generated files from completed jobs | +| **Archives** | Bulk ZIP downloads of multiple job outputs | +| **Webhooks** | Event notifications to your server | + +### Async Patterns + +Many operations are asynchronous: + +1. **Create** - Returns `202 Accepted` with resource in pending state +2. **Poll** - Check status until `ready` or `failed` +3. **Use** - Access the resource once ready + +```python +# Example: Upload from URL (async operation) +input = client.inputs.from_url("https://example.com/image.png") +# Status is "downloading" + +# Poll for completion +while input.status == "downloading": + time.sleep(1) + input = client.inputs.get(input.id) + +# Now ready to use +print(input.name) # Use this in your workflow +``` + +Or use WebSockets for real-time updates without polling. + +## Quick Example + +```python +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key="comfyui-...") + +with client: + # Run a workflow + job = client.jobs.create( + workflow={ + "3": { + "class_type": "KSampler", + "inputs": {"seed": 42, "steps": 20, ...} + }, + # ... rest of workflow + }, + tags=["my-app", "batch-1"], + ) + + # Wait for completion + while job.status == "pending": + job = client.jobs.get(job.id) + + # Get outputs + outputs = client.outputs.list(job_id=job.id) + for output in outputs.outputs: + print(f"Download: {output.download_url}") +``` + +## Next Steps + + + + Get your API key and authenticate requests + + + Full Python SDK reference and examples + + + Real-time event streaming + + + Push notifications for job events + + diff --git a/cloud/sdk/python.mdx b/cloud/sdk/python.mdx new file mode 100644 index 00000000..98fd27bc --- /dev/null +++ b/cloud/sdk/python.mdx @@ -0,0 +1,275 @@ +--- +title: "Python SDK" +description: "Official Python SDK for ComfyUI Cloud" +--- + +## Installation + +```bash +pip install comfy-cloud +``` + +For WebSocket support: + +```bash +pip install comfy-cloud[websocket] +``` + +## Quick Start + +```python +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key="comfyui-...") + +with client: + # Upload an input image + input = client.inputs.from_url("https://example.com/photo.png") + + # Create a job + job = client.jobs.create( + workflow={ + "3": {"class_type": "LoadImage", "inputs": {"image": input.name}}, + # ... rest of workflow + }, + tags=["my-project"], + ) + + # Get outputs + outputs = client.outputs.list(job_id=job.id) + for output in outputs.outputs: + print(output.download_url) +``` + +## Async Support + +All methods have async variants with `_async` suffix: + +```python +async with client: + input = await client.inputs.from_url_async("https://example.com/image.png") + job = await client.jobs.create_async(workflow={...}) + job = await client.jobs.get_async(job.id) +``` + +## API Resources + +### Inputs + +Upload and manage input files for workflows. + +```python +# Upload from URL (async - returns immediately) +input = client.inputs.from_url( + "https://example.com/image.png", + tags=["batch-1"], +) + +# Poll for ready status +input = client.inputs.get(input.id) + +# List inputs +inputs = client.inputs.list(limit=20, tags=["batch-1"]) + +# Presigned upload for large files +upload = client.inputs.get_upload_url( + name="large-image.png", + size=50_000_000, + mime_type="image/png", +) +# PUT file to upload.upload_url, then: +input = client.inputs.complete_upload(upload.id) + +# Delete +client.inputs.delete(input.id) +``` + +### Models (BYOM) + +Bring your own models - upload custom checkpoints, LoRAs, etc. + +```python +# Upload from URL +model = client.models.from_url( + url="https://civitai.com/api/download/models/123456", + type="lora", + tags=["style", "anime"], +) + +# List models +models = client.models.list(type="lora") + +# Get/delete +model = client.models.get(model.id) +client.models.delete(model.id) +``` + +### Jobs + +Execute workflows. + +```python +# Create job +job = client.jobs.create( + workflow={...}, + tags=["batch-1"], + webhook_url="https://example.com/webhook", # Optional +) + +# List jobs +jobs = client.jobs.list(status="completed", limit=50) + +# Get job +job = client.jobs.get(job.id) + +# Cancel job +job = client.jobs.cancel(job.id) +``` + +### Outputs + +Access generated files. + +```python +# List outputs for a job +outputs = client.outputs.list(job_id=job.id) + +for output in outputs.outputs: + print(f"Type: {output.type}") + print(f"URL: {output.download_url}") + +# Get specific output +output = client.outputs.get(output_id) + +# Delete +client.outputs.delete(output_id) +``` + +### Archives + +Bulk download multiple job outputs as ZIP. + +```python +# Create archive +archive = client.archives.create( + job_ids=["job_1", "job_2", "job_3"] +) + +# Poll for completion +archive = client.archives.get(archive.id) +if archive.status == "ready": + print(archive.download_url) + +# Delete +client.archives.delete(archive.id) +``` + +### Webhooks + +Manage webhook endpoints. + +```python +# Create webhook +webhook = client.webhooks.create( + url="https://example.com/webhook", + events=["job.completed", "job.failed"], +) + +# List webhooks +webhooks = client.webhooks.list() + +# Rotate secret +webhook = client.webhooks.rotate_secret(webhook.id) + +# Delete +client.webhooks.delete(webhook.id) +``` + +### Account + +```python +# Get account info +account = client.account.get() +print(f"Balance: ${account.balance / 1_000_000:.2f}") + +# Get usage stats +usage = client.account.get_usage(period="month") +print(f"Jobs completed: {usage.jobs_completed}") +print(f"GPU seconds: {usage.gpu_seconds}") +``` + +## Helper Utilities + +### Polling + +Wait for async resources to be ready: + +```python +from comfy_cloud.helpers import wait_for_ready + +# Wait for input upload +input = wait_for_ready( + get_resource=lambda: client.inputs.get(input.id), + is_ready=lambda i: i.status == "ready", + is_failed=lambda i: i.status == "failed", + timeout=120, +) + +# Wait for job completion +job = wait_for_ready( + get_resource=lambda: client.jobs.get(job.id), + is_ready=lambda j: j.status in ("completed", "failed"), + timeout=600, +) +``` + +### Presigned Uploads + +Upload large files directly to storage: + +```python +from comfy_cloud.helpers.uploads import upload_to_presigned_url + +# Get upload URL +upload = client.inputs.get_upload_url( + name="large-video.mp4", + size=500_000_000, + mime_type="video/mp4", +) + +# Upload directly +upload_to_presigned_url( + upload_url=upload.upload_url, + file=open("large-video.mp4", "rb"), + content_type="video/mp4", +) + +# Confirm +input = client.inputs.complete_upload(upload.id) +``` + +## Type Hints + +Response types are available for static analysis: + +```python +from comfy_cloud import models + +def process_job(job: models.Job) -> None: + if job.status == "completed": + print(f"Done: {job.id}") +``` + +## Error Handling + +```python +from comfy_cloud import ComfyCloudClient, models + +with client: + result = client.jobs.get("invalid-id") + + if isinstance(result, models.Error): + print(f"Error: {result.message}") + else: + print(f"Job status: {result.status}") +``` diff --git a/cloud/sdk/use-cases.mdx b/cloud/sdk/use-cases.mdx new file mode 100644 index 00000000..8c7aabf3 --- /dev/null +++ b/cloud/sdk/use-cases.mdx @@ -0,0 +1,301 @@ +--- +title: "Common Use Cases" +description: "Patterns and examples for common integration scenarios" +--- + +## Batch Processing + +Process many images with the same workflow: + +```python +import asyncio +from comfy_cloud import ComfyCloudClient +from comfy_cloud.helpers.websocket import wait_for_job + +async def process_batch(image_urls: list[str], workflow_template: dict): + client = ComfyCloudClient(api_key="comfyui-...") + + async with client: + # Upload all inputs in parallel + inputs = await asyncio.gather(*[ + client.inputs.from_url_async(url) + for url in image_urls + ]) + + # Create jobs for each input + jobs = [] + for input in inputs: + workflow = workflow_template.copy() + # Update workflow to use this input + workflow["load_image"]["inputs"]["image"] = input.name + + job = await client.jobs.create_async( + workflow=workflow, + tags=["batch", "2024-01"], + ) + jobs.append(job) + + # Wait for all jobs via WebSocket + results = await asyncio.gather(*[ + wait_for_job("comfyui-...", job.id) + for job in jobs + ]) + + return results + +# Run batch +results = asyncio.run(process_batch( + image_urls=["https://...", "https://...", ...], + workflow_template={...}, +)) +``` + +## Webhook-Based Pipeline + +For long-running jobs, use webhooks to avoid holding connections: + +```python +# Your FastAPI server +from fastapi import FastAPI, Request, HTTPException +from comfy_cloud import ComfyCloudClient +from comfy_cloud.helpers import parse_webhook + +app = FastAPI() +client = ComfyCloudClient(api_key="comfyui-...") + +@app.post("/api/generate") +async def start_generation(prompt: str): + """User-facing API that kicks off generation.""" + with client: + job = client.jobs.create( + workflow=build_workflow(prompt), + webhook_url="https://your-server.com/webhooks/comfy", + tags=["api-request"], + ) + + # Return immediately - webhook will notify on completion + return {"job_id": job.id, "status": "processing"} + +@app.post("/webhooks/comfy") +async def handle_comfy_webhook(request: Request): + """Receive completion notifications.""" + webhook = parse_webhook( + payload=await request.body(), + signature=request.headers.get("X-Comfy-Signature-256"), + timestamp=request.headers.get("X-Comfy-Timestamp"), + secret=WEBHOOK_SECRET, + ) + + if webhook.event == "job.completed": + job_id = webhook.data["id"] + outputs = webhook.data["outputs"] + + # Update your database, notify user, etc. + await save_results(job_id, outputs) + await notify_user(job_id) + + elif webhook.event == "job.failed": + await handle_failure(webhook.data["id"], webhook.data.get("error")) + + return {"status": "ok"} +``` + +## Real-Time Progress UI + +Stream progress to a frontend: + +```python +# Backend WebSocket proxy +import asyncio +from fastapi import FastAPI, WebSocket +from comfy_cloud.helpers.websocket import ComfyWebSocket + +app = FastAPI() + +@app.websocket("/ws/job/{job_id}") +async def job_progress(websocket: WebSocket, job_id: str): + await websocket.accept() + + async with ComfyWebSocket(api_key="comfyui-...") as ws: + await ws.subscribe(["job.progress", "job.completed", "job.failed"]) + + async for event in ws.events(): + if event.payload.get("id") != job_id: + continue + + await websocket.send_json({ + "event": event.event, + "data": event.payload, + }) + + if event.event in ("job.completed", "job.failed"): + break + + await websocket.close() +``` + +```javascript +// Frontend +const ws = new WebSocket(`wss://your-server.com/ws/job/${jobId}`); + +ws.onmessage = (msg) => { + const { event, data } = JSON.parse(msg.data); + + if (event === "job.progress") { + updateProgressBar(data.progress); + } else if (event === "job.completed") { + showResults(data.outputs); + } else if (event === "job.failed") { + showError(data.error); + } +}; +``` + +## Bulk Download with Archives + +Download outputs from multiple jobs as a single ZIP: + +```python +import time +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key="comfyui-...") + +with client: + # Get recent completed jobs + jobs = client.jobs.list(status="completed", limit=50) + job_ids = [j.id for j in jobs.jobs] + + # Create archive + archive = client.archives.create(job_ids=job_ids) + + # Poll for completion + while archive.status == "pending": + time.sleep(2) + archive = client.archives.get(archive.id) + + if archive.status == "ready": + print(f"Download: {archive.download_url}") + else: + print(f"Archive failed: {archive.error}") +``` + +## Using Custom Models (BYOM) + +Upload and use your own models: + +```python +from comfy_cloud import ComfyCloudClient +from comfy_cloud.helpers import wait_for_ready + +client = ComfyCloudClient(api_key="comfyui-...") + +with client: + # Upload LoRA from CivitAI + model = client.models.from_url( + url="https://civitai.com/api/download/models/123456", + type="lora", + tags=["style", "anime"], + ) + + # Wait for upload to complete + model = wait_for_ready( + get_resource=lambda: client.models.get(model.id), + is_ready=lambda m: m.status == "ready", + is_failed=lambda m: m.status == "failed", + timeout=300, + ) + + # Use in workflow + job = client.jobs.create( + workflow={ + "lora_loader": { + "class_type": "LoraLoader", + "inputs": { + "lora_name": model.name, + "strength_model": 0.8, + "strength_clip": 0.8, + }, + }, + # ... rest of workflow + }, + ) +``` + +## Tag-Based Organization + +Use tags to organize and query resources: + +```python +# Tag jobs by project and environment +job = client.jobs.create( + workflow={...}, + tags=["project:website-v2", "env:production", "user:alice"], +) + +# Query by tags +production_jobs = client.jobs.list( + tags=["env:production"], + status="completed", +) + +# Inputs inherit job tags automatically +input = client.inputs.from_url( + "https://example.com/image.png", + tags=["project:website-v2"], +) +``` + +## Error Handling & Retries + +Robust error handling with retries: + +```python +import time +from comfy_cloud import ComfyCloudClient, models + +def run_with_retry(workflow: dict, max_retries: int = 3) -> models.Job: + client = ComfyCloudClient(api_key="comfyui-...") + + with client: + for attempt in range(max_retries): + job = client.jobs.create(workflow=workflow) + + # Wait for completion + while job.status in ("pending", "running"): + time.sleep(2) + result = client.jobs.get(job.id) + + if isinstance(result, models.Error): + raise Exception(f"API error: {result.message}") + + job = result + + if job.status == "completed": + return job + + # Job failed - retry with backoff + if attempt < max_retries - 1: + delay = 2 ** attempt + print(f"Job failed, retrying in {delay}s...") + time.sleep(delay) + + raise Exception(f"Job failed after {max_retries} attempts") +``` + +## Idempotency + +Prevent duplicate jobs on network retries: + +```python +from uuid import uuid4 + +# Generate idempotency key client-side +idempotency_key = uuid4() + +# Safe to retry - same key = same job +job = client.jobs.create( + workflow={...}, + idempotency_key=idempotency_key, +) +``` diff --git a/cloud/sdk/webhooks.mdx b/cloud/sdk/webhooks.mdx new file mode 100644 index 00000000..0812d007 --- /dev/null +++ b/cloud/sdk/webhooks.mdx @@ -0,0 +1,245 @@ +--- +title: "Webhooks" +description: "Receive push notifications when events occur" +--- + +Webhooks push event notifications to your server when jobs complete, inputs are ready, or other events occur. Unlike WebSockets, webhooks don't require a persistent connection. + +## Creating a Webhook + +```python +from comfy_cloud import ComfyCloudClient + +client = ComfyCloudClient(api_key="comfyui-...") + +with client: + webhook = client.webhooks.create( + url="https://your-server.com/comfy-webhook", + events=["job.completed", "job.failed"], + ) + + # Save the secret for signature verification + print(f"Webhook ID: {webhook.id}") + print(f"Secret: {webhook.secret}") # Only shown on create! +``` + + +The webhook `secret` is only returned when creating the webhook or rotating the secret. Store it securely - you'll need it to verify signatures. + + +## Event Types + +| Event | Description | +|-------|-------------| +| `job.completed` | Job finished successfully | +| `job.failed` | Job failed with error | +| `job.cancelled` | Job was cancelled | +| `job.*` | All job events | +| `input.ready` | Input file processed | +| `input.failed` | Input upload failed | +| `input.*` | All input events | +| `model.ready` | Model upload complete | +| `model.failed` | Model upload failed | +| `model.*` | All model events | +| `archive.ready` | Archive ZIP ready | +| `archive.failed` | Archive creation failed | +| `archive.*` | All archive events | +| `account.low_balance` | Account balance is low | +| `*` | All events | + +## Webhook Payload + +```json +{ + "event": "job.completed", + "delivery_id": "dlv_abc123", + "data": { + "id": "job_xyz789", + "status": "completed", + "outputs": [ + { + "id": "out_123", + "type": "image", + "download_url": "https://storage.comfy.org/..." + } + ], + "tags": ["my-app", "batch-1"], + "created_at": "2024-01-15T10:00:00Z", + "completed_at": "2024-01-15T10:00:45Z" + } +} +``` + +## Signature Verification + +All webhooks are signed with HMAC-SHA256. **Always verify signatures** to ensure requests are from ComfyUI Cloud. + +### Headers + +| Header | Description | +|--------|-------------| +| `X-Comfy-Signature-256` | HMAC-SHA256 signature | +| `X-Comfy-Timestamp` | UNIX timestamp (seconds) | + +### Python Verification + +```python +from comfy_cloud.helpers import verify_webhook_signature, WebhookVerificationError + +@app.post("/comfy-webhook") +async def handle_webhook(request: Request): + payload = await request.body() + signature = request.headers.get("X-Comfy-Signature-256") + timestamp = request.headers.get("X-Comfy-Timestamp") + + try: + verify_webhook_signature( + payload=payload, + signature=signature, + timestamp=timestamp, + secret=WEBHOOK_SECRET, + ) + except WebhookVerificationError as e: + raise HTTPException(400, str(e)) + + # Process the webhook + data = await request.json() + if data["event"] == "job.completed": + job_id = data["data"]["id"] + outputs = data["data"]["outputs"] + # Handle completed job... + + return {"status": "ok"} +``` + +### Parse and Verify Together + +```python +from comfy_cloud.helpers import parse_webhook + +@app.post("/comfy-webhook") +async def handle_webhook(request: Request): + payload = await request.body() + + webhook = parse_webhook( + payload=payload, + signature=request.headers.get("X-Comfy-Signature-256"), + timestamp=request.headers.get("X-Comfy-Timestamp"), + secret=WEBHOOK_SECRET, + ) + + print(f"Event: {webhook.event}") + print(f"Data: {webhook.data}") + print(f"Timestamp: {webhook.timestamp}") + + return {"status": "ok"} +``` + +### Manual Verification + +The signature is computed as: + +``` +HMAC-SHA256(secret, timestamp + "." + body) +``` + +Example in Python without the SDK: + +```python +import hmac +import hashlib +import time + +def verify_signature(payload: bytes, signature: str, timestamp: str, secret: str): + # Check timestamp is recent (prevent replay attacks) + ts = int(timestamp) + if abs(time.time() - ts) > 300: # 5 minute tolerance + raise ValueError("Timestamp too old") + + # Compute expected signature + signed_payload = f"{timestamp}.".encode() + payload + expected = hmac.new( + secret.encode(), + signed_payload, + hashlib.sha256, + ).hexdigest() + + # Constant-time comparison + if not hmac.compare_digest(signature, expected): + raise ValueError("Invalid signature") +``` + +## Managing Webhooks + +### List Webhooks + +```python +webhooks = client.webhooks.list() +for wh in webhooks.webhooks: + print(f"{wh.id}: {wh.url} -> {wh.events}") +``` + +### Rotate Secret + +If your secret is compromised: + +```python +webhook = client.webhooks.rotate_secret(webhook_id) +new_secret = webhook.secret # Update your server with this +``` + +### Delete Webhook + +```python +client.webhooks.delete(webhook_id) +``` + +## Retry Policy + +Failed deliveries are retried with exponential backoff: + +| Attempt | Delay | +|---------|-------| +| 1 | Immediate | +| 2 | 1 minute | +| 3 | 5 minutes | +| 4 | 30 minutes | +| 5 | 2 hours | + +Webhooks are considered failed if: +- Your server returns a non-2xx status code +- Connection timeout (30 seconds) +- DNS resolution fails + +## Best Practices + + + + Never trust webhook payloads without verifying the signature. This prevents attackers from spoofing events. + + + + Return a 2xx response within 30 seconds. Do heavy processing asynchronously after acknowledging receipt. + + + + Use `delivery_id` to deduplicate. Retries may cause the same event to be delivered multiple times. + + + + Always use HTTPS endpoints. HTTP webhooks are rejected in production. + + + +## Per-Job Webhooks + +You can also specify a webhook URL when creating a job: + +```python +job = client.jobs.create( + workflow={...}, + webhook_url="https://your-server.com/job-webhook", +) +``` + +This webhook receives events only for that specific job, using your account's default webhook secret. diff --git a/cloud/sdk/websockets.mdx b/cloud/sdk/websockets.mdx new file mode 100644 index 00000000..94833dbb --- /dev/null +++ b/cloud/sdk/websockets.mdx @@ -0,0 +1,206 @@ +--- +title: "WebSockets" +description: "Real-time event streaming for job progress and completion" +--- + +WebSockets provide real-time updates without polling. Get instant notifications when jobs complete, inputs are ready, or errors occur. + +## Connection + +Connect to the WebSocket endpoint with your API key: + +``` +wss://cloud.comfy.org/developer/api/v1/ws?api_key=comfyui-... +``` + +## Python SDK + +```python +from comfy_cloud.helpers.websocket import ComfyWebSocket + +async with ComfyWebSocket(api_key="comfyui-...") as ws: + # Subscribe to events + await ws.subscribe(["job.completed", "job.failed"]) + + # Process events + async for event in ws.events(): + print(f"{event.event}: {event.payload['id']}") + + if event.event == "job.completed": + for output in event.payload.get("outputs", []): + print(f" {output['download_url']}") +``` + +## Event Types + +Subscribe to specific events or use wildcards: + +| Pattern | Description | +|---------|-------------| +| `job.completed` | Job finished successfully | +| `job.failed` | Job failed with error | +| `job.cancelled` | Job was cancelled | +| `job.progress` | Execution progress update | +| `job.*` | All job events | +| `input.ready` | Input file is ready | +| `input.failed` | Input upload failed | +| `input.*` | All input events | +| `model.ready` | Model upload complete | +| `model.*` | All model events | +| `*` | All events | + +## Wait for Job + +Convenience function to wait for a specific job: + +```python +from comfy_cloud.helpers.websocket import wait_for_job + +# Create job via REST API +job = client.jobs.create(workflow={...}) + +# Wait via WebSocket (more efficient than polling) +result = await wait_for_job( + api_key="comfyui-...", + job_id=job.id, + timeout=600, +) + +if result.event == "job.completed": + outputs = result.payload["outputs"] + for output in outputs: + print(output["download_url"]) +else: + print(f"Job failed: {result.payload.get('error')}") +``` + +### With Progress Callbacks + +```python +def on_progress(event): + progress = event.payload.get("progress", 0) + print(f"Progress: {progress}%") + +result = await wait_for_job( + api_key="comfyui-...", + job_id=job.id, + include_progress=True, + on_progress=on_progress, +) +``` + +## Protocol Reference + +### Connection Flow + +1. Connect with API key in query string +2. Receive `connected` message with session ID +3. Send `subscribe` to register for events +4. Receive `subscribed` confirmation +5. Receive `event` messages as they occur +6. Send `ping` periodically to keep connection alive + +### Message Formats + +**Subscribe:** +```json +{ + "type": "subscribe", + "data": { + "events": ["job.completed", "job.failed"] + } +} +``` + +**Event:** +```json +{ + "type": "event", + "timestamp": "2024-01-15T10:30:00Z", + "data": { + "event": "job.completed", + "payload": { + "id": "job_abc123", + "status": "completed", + "outputs": [ + { + "id": "out_xyz", + "type": "image", + "download_url": "https://..." + } + ] + } + } +} +``` + +**Ping/Pong:** +```json +{"type": "ping"} +{"type": "pong"} +``` + +## Auto-Reconnect + +The Python SDK handles reconnection automatically: + +```python +ws = ComfyWebSocket( + api_key="comfyui-...", + auto_reconnect=True, # Default: True + max_reconnect_attempts=5, # Default: 5 + reconnect_delay=1.0, # Initial delay (seconds) + max_reconnect_delay=30.0, # Max delay with backoff +) +``` + +Subscriptions are automatically restored after reconnection. + +## Error Handling + +```python +from comfy_cloud.helpers.websocket import ( + ComfyWebSocket, + WebSocketAuthError, + WebSocketConnectionError, +) + +try: + async with ComfyWebSocket(api_key="invalid") as ws: + await ws.subscribe(["job.*"]) +except WebSocketAuthError: + print("Invalid API key") +except WebSocketConnectionError as e: + print(f"Connection failed: {e}") +``` + +## Raw WebSocket (No SDK) + +Using any WebSocket client: + +```javascript +const ws = new WebSocket( + "wss://cloud.comfy.org/developer/api/v1/ws?api_key=comfyui-..." +); + +ws.onopen = () => { + // Subscribe to events + ws.send(JSON.stringify({ + type: "subscribe", + data: { events: ["job.*"] } + })); +}; + +ws.onmessage = (msg) => { + const data = JSON.parse(msg.data); + + if (data.type === "event") { + console.log(data.data.event, data.data.payload); + } +}; + +// Keep alive +setInterval(() => { + ws.send(JSON.stringify({ type: "ping" })); +}, 30000); +``` diff --git a/docs.json b/docs.json index da76b1df..2c2c3662 100644 --- a/docs.json +++ b/docs.json @@ -674,9 +674,39 @@ } ] }, + { + "tab": "Cloud SDK", + "pages": [ + { + "group": "Getting Started", + "pages": [ + "cloud/sdk/overview", + "cloud/sdk/authentication", + "cloud/sdk/python" + ] + }, + { + "group": "Real-Time Integration", + "pages": [ + "cloud/sdk/websockets", + "cloud/sdk/webhooks" + ] + }, + { + "group": "Guides", + "pages": [ + "cloud/sdk/use-cases" + ] + } + ] + }, { "tab": "Registry API Reference", "openapi": "https://api.comfy.org/openapi" + }, + { + "tab": "Developer API Reference", + "openapi": "openapi-developer.yaml" } ] }, diff --git a/openapi-developer.yaml b/openapi-developer.yaml new file mode 100644 index 00000000..c50a180a --- /dev/null +++ b/openapi-developer.yaml @@ -0,0 +1,2034 @@ +openapi: 3.1.0 +info: + title: ComfyUI Cloud Developer API + description: | + A developer-first, SDK-friendly API for programmatic access to ComfyUI Cloud. + + Designed for ergonomics and codegen, not constrained by ComfyUI frontend compatibility. + + ## Design Principles + + - Clean REST semantics + - Consistent async patterns (create → poll → result) + - SDK-friendly (clear resource hierarchy, good codegen) + - Arbitrary user tags for organization + - Auto-inheritance of tags where sensible + + ## Authentication + + Two authentication methods are supported: + + - **API Key**: For SDK/programmatic access. Use `Authorization: Bearer comfyui-...` + - **Firebase**: For browser-based access from Comfy-owned sites + + ## Async Patterns + + Many operations are asynchronous: + + 1. **Create** returns `202 Accepted` with resource in pending state + 2. **Poll** `GET /v1/{resource}/{id}` until status is `ready` or `failed` + 3. **Use** the resource once ready + + ## WebSocket Streaming + + Real-time event streaming is available at: + + ``` + wss://cloud.comfy.org/developer/api/v1/ws + ``` + + Connect with your API key in the `Authorization` header or as a query parameter. + Subscribe to event types (`job.*`, `input.ready`, etc.) and receive typed event payloads. + + See the Developer API V1 specification for full WebSocket protocol documentation. + Event payload schemas are defined under `components/schemas/*EventPayload`. + + ## Webhook Signatures + + Webhook requests include signature headers for verification: + + - `X-Comfy-Signature-256`: HMAC-SHA256 signature + - `X-Comfy-Timestamp`: UNIX timestamp (seconds) + + Verify by computing `HMAC-SHA256(secret, timestamp + "." + body)` and comparing. + Reject requests older than 5 minutes to prevent replay attacks. + + ## Pagination + + List endpoints use offset/limit pagination: + + - `offset`: Number of items to skip (0-based, default: 0) + - `limit`: Maximum items per page (default: 20, max: 100) + + Responses include: + - `total`: Total count matching filters + - `has_more`: Whether more items exist beyond this page + + version: 1.0.0 + contact: + name: Comfy Org + url: https://comfy.org + license: + name: Proprietary + url: https://comfy.org/terms + +servers: + - url: https://cloud.comfy.org/developer/api/v1 + description: Production + - url: https://stagingcloud.comfy.org/developer/api/v1 + description: Staging + - url: https://testcloud.comfy.org/developer/api/v1 + description: Test + - url: "{baseUrl}/api/v1" + description: Custom/Ephemeral environment + variables: + baseUrl: + default: https://cloud.comfy.org/developer + description: Base URL (e.g., https://cloud.comfy.org/developer or custom host) + +security: + - ApiKeyAuth: [] + - BearerAuth: [] + +tags: + - name: inputs + description: User-uploaded files for use in workflows (images, masks, etc.) + - name: models + description: User-uploaded models for use in workflows (BYOM) + - name: jobs + description: Workflow execution + - name: outputs + description: Generated content from job executions + - name: archives + description: Bulk download of outputs as ZIP files + - name: webhooks + description: Event notifications to external URLs + - name: account + description: Account information and settings + +paths: + # ============================================================================ + # INPUTS + # ============================================================================ + /v1/inputs: + post: + tags: [inputs] + summary: Upload input file (multipart) + operationId: createInput + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateInputMultipart' + responses: + '201': + description: Input created + content: + application/json: + schema: + $ref: '#/components/schemas/Input' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + get: + tags: [inputs] + summary: List inputs + operationId: listInputs + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + - $ref: '#/components/parameters/TagsFilter' + responses: + '200': + description: List of inputs + content: + application/json: + schema: + $ref: '#/components/schemas/InputList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/inputs/from-url: + post: + tags: [inputs] + summary: Upload input from remote URL (async) + description: | + Initiates an async download from a remote URL. Returns immediately with + `status: "downloading"`. Poll `GET /v1/inputs/{id}` for completion. + operationId: createInputFromUrl + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateInputFromUrl' + responses: + '202': + description: Download initiated + content: + application/json: + schema: + $ref: '#/components/schemas/Input' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + /v1/inputs/get-upload-url: + post: + tags: [inputs] + summary: Get presigned upload URL + description: | + Returns a presigned URL for direct upload to cloud storage. + + Flow: + 1. Call this endpoint to get `upload_url` + 2. PUT file directly to `upload_url` + 3. Call `POST /v1/inputs/{id}/complete` to finalize + + Notes: + - `size` is required to generate the presigned URL with correct content-length restriction + - Upload URL expires in 1 hour + - If upload not completed within expiry, resource transitions to `failed` status + operationId: getInputUploadUrl + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetInputUploadUrl' + responses: + '201': + description: Upload URL created + content: + application/json: + schema: + $ref: '#/components/schemas/InputUploadUrl' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + /v1/inputs/{id}: + get: + tags: [inputs] + summary: Get input details + operationId: getInput + parameters: + - $ref: '#/components/parameters/InputId' + responses: + '200': + description: Input details + content: + application/json: + schema: + $ref: '#/components/schemas/Input' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + delete: + tags: [inputs] + summary: Delete input + operationId: deleteInput + parameters: + - $ref: '#/components/parameters/InputId' + responses: + '204': + description: Input deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/inputs/{id}/complete: + post: + tags: [inputs] + summary: Confirm presigned upload complete + description: | + Called after uploading directly to the presigned URL. + Validates the file exists and computes hash. + operationId: completeInputUpload + parameters: + - $ref: '#/components/parameters/InputId' + responses: + '200': + description: Upload confirmed + content: + application/json: + schema: + $ref: '#/components/schemas/Input' + '400': + description: Upload not found in storage + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # MODELS + # ============================================================================ + /v1/models: + post: + tags: [models] + summary: Upload model file (multipart) + operationId: createModel + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: '#/components/schemas/CreateModelMultipart' + responses: + '201': + description: Model created + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + get: + tags: [models] + summary: List models + operationId: listModels + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + - $ref: '#/components/parameters/TagsFilter' + - name: type + in: query + description: Filter by model type + schema: + $ref: '#/components/schemas/ModelType' + responses: + '200': + description: List of models + content: + application/json: + schema: + $ref: '#/components/schemas/ModelList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/models/from-url: + post: + tags: [models] + summary: Upload model from remote URL (async) + description: | + Initiates an async download from a remote URL (Civitai, HuggingFace, etc.). + + For Civitai/HuggingFace URLs, metadata is auto-extracted: + - Civitai: `base_model`, `trained_words`, `source_arn`, `preview_url` + - HuggingFace: `repo_id`, `revision`, `source_arn` + + You can override auto-extracted metadata by providing explicit values. + + When downloading from Civitai/HuggingFace, `source` is automatically populated + with verified provenance. You cannot set `source` manually. + operationId: createModelFromUrl + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateModelFromUrl' + responses: + '202': + description: Download initiated + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + /v1/models/get-upload-url: + post: + tags: [models] + summary: Get presigned upload URL for model + description: | + Returns a presigned URL for direct upload to cloud storage. + + Notes: + - `size` is required to generate the presigned URL with correct content-length restriction + - Upload URL expires in 1 hour + - If upload not completed within expiry, resource transitions to `failed` status + operationId: getModelUploadUrl + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GetModelUploadUrl' + responses: + '201': + description: Upload URL created + content: + application/json: + schema: + $ref: '#/components/schemas/ModelUploadUrl' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + /v1/models/types: + get: + tags: [models] + summary: List valid model types + operationId: listModelTypes + responses: + '200': + description: List of valid model types + content: + application/json: + schema: + type: object + properties: + types: + type: array + items: + $ref: '#/components/schemas/ModelType' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/models/{id}: + get: + tags: [models] + summary: Get model details + operationId: getModel + parameters: + - $ref: '#/components/parameters/ModelId' + responses: + '200': + description: Model details + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + delete: + tags: [models] + summary: Delete model + operationId: deleteModel + parameters: + - $ref: '#/components/parameters/ModelId' + responses: + '204': + description: Model deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/models/{id}/complete: + post: + tags: [models] + summary: Confirm presigned upload complete + operationId: completeModelUpload + parameters: + - $ref: '#/components/parameters/ModelId' + responses: + '200': + description: Upload confirmed + content: + application/json: + schema: + $ref: '#/components/schemas/Model' + '400': + description: Upload not found in storage + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # JOBS + # ============================================================================ + /v1/jobs: + post: + tags: [jobs] + summary: Create a job + description: | + Submit a workflow for execution. The job is queued and executed asynchronously. + + Poll `GET /v1/jobs/{id}` for status updates, or use WebSocket/webhooks for real-time updates. + operationId: createJob + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateJob' + responses: + '202': + description: Job queued + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + get: + tags: [jobs] + summary: List jobs + operationId: listJobs + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + - $ref: '#/components/parameters/TagsFilter' + - name: status + in: query + description: Filter by status + schema: + $ref: '#/components/schemas/JobStatus' + - name: batch_id + in: query + description: Filter by batch ID + schema: + type: string + responses: + '200': + description: List of jobs + content: + application/json: + schema: + $ref: '#/components/schemas/JobList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/jobs/batch: + post: + tags: [jobs] + summary: Create multiple jobs in a batch + description: | + Submit multiple jobs at once (max 50). All jobs share a `batch_id` for tracking. + + Each job in the batch can have its own workflow and tags. + operationId: createJobBatch + parameters: + - $ref: '#/components/parameters/IdempotencyKey' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateJobBatch' + responses: + '202': + description: Batch queued + content: + application/json: + schema: + $ref: '#/components/schemas/JobBatch' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + /v1/jobs/{id}: + get: + tags: [jobs] + summary: Get job details + operationId: getJob + parameters: + - $ref: '#/components/parameters/JobId' + responses: + '200': + description: Job details + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/jobs/{id}/cancel: + post: + tags: [jobs] + summary: Cancel a job + description: | + Attempts to cancel a pending or running job. + + - `pending` jobs are cancelled immediately + - `running` jobs are cancelled at the next checkpoint (may still complete) + - `completed`, `failed`, `cancelled` jobs cannot be cancelled + operationId: cancelJob + parameters: + - $ref: '#/components/parameters/JobId' + responses: + '200': + description: Job cancelled (or cancellation initiated) + content: + application/json: + schema: + $ref: '#/components/schemas/Job' + '400': + description: Job cannot be cancelled + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/jobs/{id}/outputs: + get: + tags: [jobs] + summary: List outputs for a job + operationId: listJobOutputs + parameters: + - $ref: '#/components/parameters/JobId' + responses: + '200': + description: List of outputs + content: + application/json: + schema: + $ref: '#/components/schemas/OutputList' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # OUTPUTS + # ============================================================================ + /v1/outputs: + get: + tags: [outputs] + summary: List outputs + operationId: listOutputs + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + - $ref: '#/components/parameters/TagsFilter' + - name: job_id + in: query + description: Filter by job ID + schema: + type: string + - name: type + in: query + description: Filter by output type + schema: + $ref: '#/components/schemas/OutputType' + responses: + '200': + description: List of outputs + content: + application/json: + schema: + $ref: '#/components/schemas/OutputList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/outputs/{id}: + get: + tags: [outputs] + summary: Get output details + operationId: getOutput + parameters: + - $ref: '#/components/parameters/OutputId' + responses: + '200': + description: Output details + content: + application/json: + schema: + $ref: '#/components/schemas/Output' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + delete: + tags: [outputs] + summary: Delete output + operationId: deleteOutput + parameters: + - $ref: '#/components/parameters/OutputId' + responses: + '204': + description: Output deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # ARCHIVES + # ============================================================================ + /v1/archives: + post: + tags: [archives] + summary: Create archive from job outputs + description: | + Creates a ZIP archive containing outputs from specified jobs. + + Archive creation is async. Poll `GET /v1/archives/{id}` for completion. + Archives expire after 24 hours. + operationId: createArchive + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateArchive' + responses: + '202': + description: Archive creation initiated + content: + application/json: + schema: + $ref: '#/components/schemas/Archive' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + get: + tags: [archives] + summary: List archives + operationId: listArchives + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + responses: + '200': + description: List of archives + content: + application/json: + schema: + $ref: '#/components/schemas/ArchiveList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/archives/{id}: + get: + tags: [archives] + summary: Get archive details + operationId: getArchive + parameters: + - $ref: '#/components/parameters/ArchiveId' + responses: + '200': + description: Archive details + content: + application/json: + schema: + $ref: '#/components/schemas/Archive' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + delete: + tags: [archives] + summary: Delete archive + operationId: deleteArchive + parameters: + - $ref: '#/components/parameters/ArchiveId' + responses: + '204': + description: Archive deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # WEBHOOKS + # ============================================================================ + /v1/webhooks: + post: + tags: [webhooks] + summary: Create webhook + operationId: createWebhook + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWebhook' + responses: + '201': + description: Webhook created + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + + get: + tags: [webhooks] + summary: List webhooks + operationId: listWebhooks + parameters: + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + responses: + '200': + description: List of webhooks + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookList' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/webhooks/{id}: + get: + tags: [webhooks] + summary: Get webhook details + operationId: getWebhook + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Webhook details + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + patch: + tags: [webhooks] + summary: Update webhook + operationId: updateWebhook + parameters: + - $ref: '#/components/parameters/WebhookId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWebhook' + responses: + '200': + description: Webhook updated + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + '422': + $ref: '#/components/responses/ValidationError' + + delete: + tags: [webhooks] + summary: Delete webhook + operationId: deleteWebhook + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '204': + description: Webhook deleted + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/webhooks/{id}/rotate-secret: + post: + tags: [webhooks] + summary: Rotate webhook signing secret + operationId: rotateWebhookSecret + parameters: + - $ref: '#/components/parameters/WebhookId' + responses: + '200': + description: Secret rotated + content: + application/json: + schema: + $ref: '#/components/schemas/Webhook' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/webhooks/{id}/deliveries: + get: + tags: [webhooks] + summary: List webhook deliveries + description: Returns recent delivery attempts for debugging + operationId: listWebhookDeliveries + parameters: + - $ref: '#/components/parameters/WebhookId' + - $ref: '#/components/parameters/PageOffset' + - $ref: '#/components/parameters/PageLimit' + responses: + '200': + description: List of deliveries + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookDeliveryList' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + /v1/webhooks/{id}/deliveries/{deliveryId}/resend: + post: + tags: [webhooks] + summary: Resend a webhook delivery + operationId: resendWebhookDelivery + parameters: + - $ref: '#/components/parameters/WebhookId' + - name: deliveryId + in: path + required: true + schema: + type: string + responses: + '202': + description: Resend initiated + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookDelivery' + '401': + $ref: '#/components/responses/Unauthorized' + '404': + $ref: '#/components/responses/NotFound' + + # ============================================================================ + # WORKFLOWS + # ============================================================================ + /v1/workflows/validate: + post: + tags: [jobs] + summary: Validate a workflow + description: | + Validates a workflow without executing it. Returns validation errors if any. + + Use this to check workflows before submitting jobs. + operationId: validateWorkflow + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ValidateWorkflow' + responses: + '200': + description: Validation result + content: + application/json: + schema: + $ref: '#/components/schemas/WorkflowValidation' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + + # ============================================================================ + # ACCOUNT + # ============================================================================ + /v1/account: + get: + tags: [account] + summary: Get account information + operationId: getAccount + responses: + '200': + description: Account information + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/account/usage: + get: + tags: [account] + summary: Get account usage summary + operationId: getAccountUsage + parameters: + - name: period + in: query + description: Usage period + schema: + type: string + enum: [day, week, month] + default: month + responses: + '200': + description: Usage summary + content: + application/json: + schema: + $ref: '#/components/schemas/AccountUsage' + '401': + $ref: '#/components/responses/Unauthorized' + + /v1/account/low-balance: + get: + tags: [account] + summary: Get low balance alert settings + operationId: getLowBalanceSettings + responses: + '200': + description: Low balance settings + content: + application/json: + schema: + $ref: '#/components/schemas/LowBalanceSettings' + '401': + $ref: '#/components/responses/Unauthorized' + + put: + tags: [account] + summary: Update low balance alert settings + operationId: updateLowBalanceSettings + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/LowBalanceSettings' + responses: + '200': + description: Settings updated + content: + application/json: + schema: + $ref: '#/components/schemas/LowBalanceSettings' + '400': + $ref: '#/components/responses/BadRequest' + '401': + $ref: '#/components/responses/Unauthorized' + '422': + $ref: '#/components/responses/ValidationError' + +components: + securitySchemes: + ApiKeyAuth: + type: http + scheme: bearer + description: | + API key authentication. Keys are prefixed with `comfyui-`. + + Example: `Authorization: Bearer comfyui-abc123...` + + BearerAuth: + type: http + scheme: bearer + description: Firebase ID token for browser-based access from Comfy-owned sites. + + parameters: + PageOffset: + name: offset + in: query + description: Number of items to skip (0-based) + schema: + type: integer + minimum: 0 + default: 0 + + PageLimit: + name: limit + in: query + description: Maximum number of items to return (default 20, max 100) + schema: + type: integer + minimum: 1 + maximum: 100 + default: 20 + + TagsFilter: + name: tags + in: query + description: Filter by tags (comma-separated, AND logic) + schema: + type: string + example: "project-x,batch-123" + + IdempotencyKey: + name: Idempotency-Key + in: header + description: Unique key for idempotent requests + schema: + type: string + format: uuid + + InputId: + name: id + in: path + required: true + description: Input ID (prefixed with `inp_`) + schema: + type: string + example: inp_abc123 + + ModelId: + name: id + in: path + required: true + description: Model ID (prefixed with `mod_`) + schema: + type: string + example: mod_xyz789 + + JobId: + name: id + in: path + required: true + description: Job ID (prefixed with `job_`) + schema: + type: string + example: job_abc123 + + OutputId: + name: id + in: path + required: true + description: Output ID (prefixed with `out_`) + schema: + type: string + example: out_def456 + + ArchiveId: + name: id + in: path + required: true + description: Archive ID (prefixed with `arc_`) + schema: + type: string + example: arc_ghi789 + + WebhookId: + name: id + in: path + required: true + description: Webhook ID (prefixed with `whk_`) + schema: + type: string + example: whk_abc123 + + responses: + BadRequest: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + Unauthorized: + description: Unauthorized - invalid or missing authentication + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + NotFound: + description: Resource not found + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + + ValidationError: + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorResponse' + + schemas: + # ========================================================================== + # COMMON + # ========================================================================== + ErrorCode: + type: string + description: Machine-readable error codes + enum: + - upload_not_found + - download_failed + - invalid_request + - validation_error + - not_found + - unauthorized + - forbidden + - rate_limited + - internal_error + + Error: + type: object + required: [error] + properties: + error: + type: object + required: [code, message] + properties: + code: + $ref: '#/components/schemas/ErrorCode' + message: + type: string + description: Human-readable error message + + ValidationErrorResponse: + type: object + required: [error] + properties: + error: + type: object + required: [code, message, details] + properties: + code: + type: string + example: validation_error + message: + type: string + example: Request validation failed + details: + type: array + items: + type: object + properties: + field: + type: string + message: + type: string + + PaginationMeta: + type: object + required: [total, has_more] + properties: + total: + type: integer + description: Total number of items matching the filters + has_more: + type: boolean + description: Whether more items are available beyond this page + + Tags: + type: array + items: + type: string + description: Arbitrary user tags for organization + example: ["project-x", "batch-123"] + + # ========================================================================== + # INPUT SCHEMAS + # ========================================================================== + InputStatus: + type: string + enum: [pending_upload, uploading, downloading, ready, failed] + description: | + - `pending_upload` - presigned URL issued, awaiting direct upload + - `uploading` - multipart upload in progress + - `downloading` - URL download in progress + - `ready` - available for use + - `failed` - upload/download failed + + Input: + type: object + required: [id, name, status, created_at] + properties: + id: + type: string + example: inp_abc123 + name: + type: string + example: reference.png + status: + $ref: '#/components/schemas/InputStatus' + size: + type: integer + format: int64 + description: File size in bytes + mime_type: + type: string + example: image/png + tags: + $ref: '#/components/schemas/Tags' + download_url: + type: string + format: uri + description: Signed URL for downloading (only when status is ready) + error: + $ref: '#/components/schemas/ResourceError' + created_at: + type: string + format: date-time + + InputList: + type: object + required: [inputs, total, has_more] + properties: + inputs: + type: array + items: + $ref: '#/components/schemas/Input' + total: + type: integer + description: Total number of inputs matching the filters + has_more: + type: boolean + description: Whether more inputs are available beyond this page + + CreateInputMultipart: + type: object + required: [file] + properties: + file: + type: string + format: binary + name: + type: string + description: Override filename + tags: + type: string + description: Comma-separated tags + + CreateInputFromUrl: + type: object + required: [url] + properties: + url: + type: string + format: uri + description: URL to download from + name: + type: string + description: Override filename + tags: + $ref: '#/components/schemas/Tags' + + GetInputUploadUrl: + type: object + required: [name, mime_type, size] + properties: + name: + type: string + example: large-image.png + mime_type: + type: string + example: image/png + size: + type: integer + format: int64 + description: File size in bytes (required for presigned URL) + tags: + $ref: '#/components/schemas/Tags' + + InputUploadUrl: + type: object + required: [id, status, upload_url, upload_expires_at] + properties: + id: + type: string + example: inp_abc123 + status: + type: string + enum: [pending_upload] + upload_url: + type: string + format: uri + description: Presigned URL for direct upload + upload_expires_at: + type: string + format: date-time + description: When the upload URL expires + + ResourceError: + type: object + properties: + code: + type: string + example: download_failed + message: + type: string + example: HTTP 404 while downloading from URL + + # ========================================================================== + # MODEL SCHEMAS + # ========================================================================== + ModelType: + type: string + enum: [checkpoint, lora, vae, controlnet, embedding, upscaler, clip, unet, diffusion_model] + description: Type of model + + ModelStatus: + type: string + enum: [pending_upload, uploading, downloading, ready, failed] + + ModelMetadata: + type: object + properties: + display_name: + type: string + description: Human-readable model name + base_model: + type: string + description: Base model compatibility (e.g., "SD 1.5", "SDXL 1.0", "Flux.1") + trained_words: + type: array + items: + type: string + description: Trigger words/tokens for LoRAs and embeddings + description: + type: string + preview_url: + type: string + format: uri + description: URL to preview image. Mutually exclusive with preview_image. + preview_image: + type: string + description: | + Base64-encoded preview image (data URL format, e.g., "data:image/jpeg;base64,..."). + Mutually exclusive with preview_url. If both provided, preview_image takes precedence. + + ModelSource: + type: object + description: | + Verified provenance information for the model. + Read-only: automatically populated for from-url uploads, null for manual uploads. + readOnly: true + properties: + url: + type: string + format: uri + description: Original download URL + arn: + type: string + description: Source identifier (e.g., "civitai:model:123456:version:789012:file:999") + required: [url, arn] + + Model: + type: object + required: [id, name, type, status, created_at] + properties: + id: + type: string + example: mod_xyz789 + name: + type: string + example: my-lora.safetensors + type: + $ref: '#/components/schemas/ModelType' + status: + $ref: '#/components/schemas/ModelStatus' + size: + type: integer + format: int64 + mime_type: + type: string + tags: + $ref: '#/components/schemas/Tags' + metadata: + $ref: '#/components/schemas/ModelMetadata' + source: + $ref: '#/components/schemas/ModelSource' + download_url: + type: string + format: uri + error: + $ref: '#/components/schemas/ResourceError' + created_at: + type: string + format: date-time + + ModelList: + type: object + required: [models, total, has_more] + properties: + models: + type: array + items: + $ref: '#/components/schemas/Model' + total: + type: integer + description: Total number of models matching the filters + has_more: + type: boolean + description: Whether more models are available beyond this page + + CreateModelMultipart: + type: object + required: [file, type] + properties: + file: + type: string + format: binary + type: + $ref: '#/components/schemas/ModelType' + name: + type: string + tags: + type: string + description: Comma-separated tags + metadata: + type: string + description: JSON-encoded metadata + + CreateModelFromUrl: + type: object + required: [url, type] + properties: + url: + type: string + format: uri + type: + $ref: '#/components/schemas/ModelType' + name: + type: string + tags: + $ref: '#/components/schemas/Tags' + metadata: + $ref: '#/components/schemas/ModelMetadata' + + GetModelUploadUrl: + type: object + required: [name, type, mime_type, size] + properties: + name: + type: string + type: + $ref: '#/components/schemas/ModelType' + mime_type: + type: string + size: + type: integer + format: int64 + tags: + $ref: '#/components/schemas/Tags' + metadata: + $ref: '#/components/schemas/ModelMetadata' + + ModelUploadUrl: + type: object + required: [id, status, upload_url, upload_expires_at] + properties: + id: + type: string + status: + type: string + enum: [pending_upload] + upload_url: + type: string + format: uri + upload_expires_at: + type: string + format: date-time + + # ========================================================================== + # JOB SCHEMAS + # ========================================================================== + JobStatus: + type: string + enum: [pending, running, completed, failed, cancelled] + + Job: + type: object + required: [id, status, created_at] + properties: + id: + type: string + example: job_abc123 + status: + $ref: '#/components/schemas/JobStatus' + workflow: + type: object + description: The submitted workflow (API format) + tags: + $ref: '#/components/schemas/Tags' + batch_id: + type: string + description: Batch ID if part of a batch + outputs: + type: array + items: + $ref: '#/components/schemas/Output' + description: Outputs (only when completed) + usage: + $ref: '#/components/schemas/JobUsage' + error: + $ref: '#/components/schemas/ResourceError' + created_at: + type: string + format: date-time + started_at: + type: string + format: date-time + completed_at: + type: string + format: date-time + + JobList: + type: object + required: [jobs, total, has_more] + properties: + jobs: + type: array + items: + $ref: '#/components/schemas/Job' + total: + type: integer + description: Total number of jobs matching the filters + has_more: + type: boolean + description: Whether more jobs are available beyond this page + + CreateJob: + type: object + required: [workflow] + properties: + workflow: + type: object + description: ComfyUI workflow in API format + tags: + $ref: '#/components/schemas/Tags' + + CreateJobBatch: + type: object + required: [jobs] + properties: + jobs: + type: array + items: + $ref: '#/components/schemas/CreateJob' + minItems: 1 + maxItems: 50 + description: Array of jobs (max 50) + + JobBatch: + type: object + required: [batch_id, jobs] + properties: + batch_id: + type: string + example: bat_xyz789 + jobs: + type: array + items: + $ref: '#/components/schemas/Job' + + JobUsage: + type: object + properties: + events: + type: array + items: + type: object + properties: + timestamp: + type: string + format: date-time + event_type: + type: string + example: gpu_compute + properties: + type: object + additionalProperties: true + + # ========================================================================== + # OUTPUT SCHEMAS + # ========================================================================== + OutputType: + type: string + enum: [image, video, audio, text, file] + + Output: + type: object + required: [id, name, type, job_id, created_at] + properties: + id: + type: string + example: out_def456 + name: + type: string + example: ComfyUI_00001_.png + type: + $ref: '#/components/schemas/OutputType' + job_id: + type: string + size: + type: integer + format: int64 + mime_type: + type: string + tags: + $ref: '#/components/schemas/Tags' + download_url: + type: string + format: uri + created_at: + type: string + format: date-time + + OutputList: + type: object + required: [outputs, total, has_more] + properties: + outputs: + type: array + items: + $ref: '#/components/schemas/Output' + total: + type: integer + description: Total number of outputs matching the filters + has_more: + type: boolean + description: Whether more outputs are available beyond this page + + # ========================================================================== + # ARCHIVE SCHEMAS + # ========================================================================== + ArchiveStatus: + type: string + enum: [pending, ready, failed, expired] + + Archive: + type: object + required: [id, status, created_at] + properties: + id: + type: string + example: arc_ghi789 + status: + $ref: '#/components/schemas/ArchiveStatus' + job_ids: + type: array + items: + type: string + size: + type: integer + format: int64 + download_url: + type: string + format: uri + expires_at: + type: string + format: date-time + error: + $ref: '#/components/schemas/ResourceError' + created_at: + type: string + format: date-time + + ArchiveList: + type: object + required: [archives, total, has_more] + properties: + archives: + type: array + items: + $ref: '#/components/schemas/Archive' + total: + type: integer + description: Total number of archives matching the filters + has_more: + type: boolean + description: Whether more archives are available beyond this page + + CreateArchive: + type: object + required: [job_ids] + properties: + job_ids: + type: array + items: + type: string + minItems: 1 + maxItems: 100 + description: Job IDs to include in archive + + # ========================================================================== + # WEBHOOK SCHEMAS + # ========================================================================== + WebhookEvent: + type: string + enum: + - job.completed + - job.failed + - job.cancelled + - job.* + - input.ready + - input.failed + - input.* + - model.ready + - model.failed + - model.* + - archive.ready + - archive.failed + - archive.* + - account.low_balance + - account.* + - "*" + + Webhook: + type: object + required: [id, url, events, enabled, created_at] + properties: + id: + type: string + example: whk_abc123 + url: + type: string + format: uri + events: + type: array + items: + $ref: '#/components/schemas/WebhookEvent' + secret: + type: string + description: Signing secret (only returned on create/rotate) + enabled: + type: boolean + description: + type: string + created_at: + type: string + format: date-time + + WebhookList: + type: object + required: [webhooks, total, has_more] + properties: + webhooks: + type: array + items: + $ref: '#/components/schemas/Webhook' + total: + type: integer + description: Total number of webhooks + has_more: + type: boolean + description: Whether more webhooks are available beyond this page + + CreateWebhook: + type: object + required: [url, events] + properties: + url: + type: string + format: uri + events: + type: array + items: + $ref: '#/components/schemas/WebhookEvent' + minItems: 1 + description: + type: string + + UpdateWebhook: + type: object + properties: + url: + type: string + format: uri + events: + type: array + items: + $ref: '#/components/schemas/WebhookEvent' + enabled: + type: boolean + description: + type: string + + WebhookDeliveryStatus: + type: string + enum: [pending, success, failed] + + WebhookDelivery: + type: object + required: [id, event, status, created_at] + properties: + id: + type: string + event: + type: string + payload: + type: object + status: + $ref: '#/components/schemas/WebhookDeliveryStatus' + response_status: + type: integer + description: HTTP status code from target + response_body: + type: string + description: Response body (truncated) + attempts: + type: integer + next_retry_at: + type: string + format: date-time + created_at: + type: string + format: date-time + + WebhookDeliveryList: + type: object + required: [deliveries, total, has_more] + properties: + deliveries: + type: array + items: + $ref: '#/components/schemas/WebhookDelivery' + total: + type: integer + description: Total number of deliveries + has_more: + type: boolean + description: Whether more deliveries are available beyond this page + + # ========================================================================== + # WORKFLOW VALIDATION + # ========================================================================== + ValidateWorkflow: + type: object + required: [workflow] + properties: + workflow: + type: object + description: ComfyUI workflow to validate + + WorkflowValidation: + type: object + required: [valid] + properties: + valid: + type: boolean + errors: + type: array + items: + type: object + properties: + node_id: + type: string + field: + type: string + message: + type: string + + # ========================================================================== + # ACCOUNT SCHEMAS + # ========================================================================== + Account: + type: object + required: [id, email, balance] + properties: + id: + type: string + email: + type: string + format: email + balance: + type: integer + format: int64 + description: Account balance in micros (1/1,000,000 of a dollar) + created_at: + type: string + format: date-time + + AccountUsage: + type: object + properties: + period: + type: string + enum: [day, week, month] + jobs_completed: + type: integer + jobs_failed: + type: integer + gpu_seconds: + type: number + format: double + credits_used: + type: integer + format: int64 + description: Credits used in micros + + LowBalanceSettings: + type: object + properties: + enabled: + type: boolean + description: Whether low balance alerts are enabled + threshold: + type: integer + format: int64 + description: Alert when balance drops below this (micros) + email_enabled: + type: boolean + description: Send email notifications