Link copied

I’ve been building workflows on Comfy Cloud and poking around a lot.
Running them one by one in the browser is fine, but when you want the same workflow from a script or an app, you need the API.
This post walks through what I actually tried, based on the Cloud API Overview.

Comfy Cloud official site

This post includes ComfyUI affiliate / invite links.


What you’ll get from this post

  • How to call a Comfy Cloud workflow as an API
  • How to download outputs with tools like Postman or Python

Prerequisites & notes

Before you hit the API, keep these in mind.

ItemDetail
PlanAPI execution needs a paid plan (Standard / Creator / Pro). The Free tier cannot run workflows via API (docs)
API statusMarked Experimental — endpoints and response shapes may change
CreditsUses the same monthly credits as the Web UI (no separate API pool)
Base URLhttps://cloud.comfy.org
AuthEvery request needs an X-API-Key header

No account yet? You can sign up with the invite link below.

👉 Sign up for Comfy Cloud (invite link)


Overall flow

The official Quick Start is basically four steps:

  1. Prepare the workflow in API format (build in the browser, then export)
  2. POST /api/prompt to submit the job (rewrite inputs if needed)
  3. Wait for completion via polling or WebSocket
  4. Fetch the output with /api/view (or the job-details API)
flowchart LR
  A[Build workflow in browser] --> B[Export as API format]
  B --> C["POST /api/prompt"]
  C --> D[Watch progress]
  D --> E["Download via /api/view"]

Step 1. Build a workflow on Comfy Cloud

Start in the browser on Comfy Cloud and pick a template.
I used Z-Image-Turbo (text-to-image) under the Image category.

Picking an image template

Some templates are partner workflows with an api_ prefix (edits, etc.).
Here’s an example of an API-oriented graph.

Sample API-oriented workflow

For the walkthrough I used a simple test-api workflow.

  • Make sure it reaches a SaveImage node (so you don’t miss outputs)
  • Note which node inputs you’ll override later (prompt, seed, …)
  • If you use Partner Nodes, confirm they run cleanly in the browser first

Step 2. Export the workflow in API format

The JSON you send to the Cloud API is not the normal save format.
Per Workflow API Format, you must export API format.

AspectSave formatAPI format (Export Workflow API)
MenuFile → SaveFile → Export Workflow (API)
Node keysTitles / labelsNumeric node IDs
UI metadataPositions, colors, groups, …Stripped
UseRe-edit in the editorAPI submission

Export steps:

  1. Open the workflow in the editor
  2. Choose File → Export Workflow (API) (Japanese UI: ファイル → エクスポート (API))
  3. Save the downloaded JSON (here: test-api.json)
Choosing Export (API) from the file menu

For test-api, the exported JSON looks like this (from test-api.json):

{
  "9": {
    "inputs": {
      "filename_prefix": "test_api",
      "images": ["57:8", 0]
    },
    "class_type": "SaveImage",
    "_meta": {
      "title": "画像を保存"
    }
  },
  "62": {
    "inputs": {
      "filename_prefix": "ComfyUI",
      "format": "png",
      "format.bit_depth": "8-bit",
      "format.input_color_space": "sRGB"
    },
    "class_type": "SaveImageAdvanced",
    "_meta": {
      "title": "Save Image (Advanced)"
    }
  },
  "57:30": {
    "inputs": {
      "clip_name": "qwen_3_4b.safetensors",
      "type": "lumina2",
      "device": "default"
    },
    "class_type": "CLIPLoader",
    "_meta": {
      "title": "CLIPを読み込む"
    }
  },
  "57:29": {
    "inputs": {
      "vae_name": "ae.safetensors"
    },
    "class_type": "VAELoader",
    "_meta": {
      "title": "VAEを読み込む"
    }
  },
  "57:33": {
    "inputs": {
      "conditioning": ["57:27", 0]
    },
    "class_type": "ConditioningZeroOut",
    "_meta": {
      "title": "条件付けゼロアウト"
    }
  },
  "57:8": {
    "inputs": {
      "samples": ["57:3", 0],
      "vae": ["57:29", 0]
    },
    "class_type": "VAEDecode",
    "_meta": {
      "title": "VAEデコード"
    }
  },
  "57:28": {
    "inputs": {
      "unet_name": "z_image_turbo_bf16.safetensors",
      "weight_dtype": "default"
    },
    "class_type": "UNETLoader",
    "_meta": {
      "title": "拡散モデルを読み込む"
    }
  },
  "57:27": {
    "inputs": {
      "text": "input text",
      "clip": ["57:30", 0]
    },
    "class_type": "CLIPTextEncode",
    "_meta": {
      "title": "CLIPテキストエンコード(プロンプト)"
    }
  },
  "57:13": {
    "inputs": {
      "width": 1024,
      "height": 1024,
      "batch_size": 1
    },
    "class_type": "EmptySD3LatentImage",
    "_meta": {
      "title": "空のSD3潜在画像"
    }
  },
  "57:11": {
    "inputs": {
      "shift": 3,
      "model": ["57:28", 0]
    },
    "class_type": "ModelSamplingAuraFlow",
    "_meta": {
      "title": "モデルサンプリングオーラフロー"
    }
  },
  "57:3": {
    "inputs": {
      "seed": 0,
      "steps": 8,
      "cfg": 1,
      "sampler_name": "res_multistep",
      "scheduler": "simple",
      "denoise": 1,
      "model": ["57:11", 0],
      "positive": ["57:27", 0],
      "negative": ["57:33", 0],
      "latent_image": ["57:13", 0]
    },
    "class_type": "KSampler",
    "_meta": {
      "title": "Kサンプラー"
    }
  }
}

Handy node IDs to override:

GoalNode IDField
Change prompt"57:27"inputs.text
Change seed"57:3"inputs.seed
Output prefix"9"inputs.filename_prefix

Step 3. Get an API key

Cloud API auth uses a Comfy Platform API key.
Steps are in Getting an API Key.

  1. Log in at platform.comfy.org/login
  2. In API Keys, click + New API Key
  3. Name it and Generate
  4. Copy the key immediately (it won’t be shown again)
API Keys list and New API Key button
Generate API key dialog
Key shown once after creation

Store it in an env var:

export COMFY_CLOUD_API_KEY="comfyui-xxxxxxxxxxxx"
export BASE_URL="https://cloud.comfy.org"

Smoke-test with the user info endpoint.
I started in Postman.

curl -X GET "$BASE_URL/api/user" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

In Postman:

  1. Method GET, URL https://cloud.comfy.org/api/user
  2. Authorization → Type API Key
  3. Key X-API-Key, Value = your key, Add to Header
  4. Send

200 OK with something like {"id":"...","status":"active"} means the key works.

Postman GET /api/user returning 200 OK

Step 4. Pass inputs and run the workflow via API

This is the main part.
Per the official Quick Start, send API-format JSON in the prompt field of POST /api/prompt.

Submit from Postman (before writing code)

You can hit the same endpoint from Postman first.
Important: don’t paste exported test-api.json as the raw body root — wrap it in prompt.

  1. Method POST, URL https://cloud.comfy.org/api/prompt
  2. Auth same as Step 3 (X-API-Key header)
  3. BodyrawJSON
  4. Send a body shaped like this (put the entire test-api.json inside prompt; the snippet below is structural):
{
  "prompt": {
    "9": { "...": "SaveImage nodes, etc. — full test-api.json contents" },
    "57:27": {
      "inputs": {
        "text": "rewrite with any prompt you want",
        "clip": ["57:30", 0]
      },
      "class_type": "CLIPTextEncode"
    }
  }
}

The easy footgun is pasting test-api.json at the body root.
Always use {"prompt": { ...contents of test-api.json... }}.
To change the text prompt, edit "57:27"inputs.text in the body, then Send.

On success you get a prompt_id, same as curl / Python:

{
  "prompt_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "number": 1,
  "node_errors": {}
}

You can also check progress in Postman:

  • GET https://cloud.comfy.org/api/job/{prompt_id}/status
  • Same X-API-Key auth

That’s enough to validate keys and body shape before writing code.

Submit as-is (curl)

curl -X POST "$BASE_URL/api/prompt" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"prompt": '"$(cat test-api.json)"'}'

Typical success response:

{
  "prompt_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "number": 1,
  "node_errors": {}
}

prompt_id is the job ID for monitoring and downloads.

From Postman it looked like this:

Postman POST /api/prompt success with prompt_id

Rewrite inputs, then submit (Python)

In real use you usually load the export and swap seed / prompt before sending.
The official Complete Example does the same.

import os
import json
import requests

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"}

with open("test-api.json") as f:
    workflow = json.load(f)

# Node IDs must match your test-api.json (they differ per workflow)
workflow["57:3"]["inputs"]["seed"] = 42
workflow["57:27"]["inputs"]["text"] = "a cat wearing sunglasses, studio photo"

response = requests.post(
    f"{BASE_URL}/api/prompt",
    headers=get_headers(),
    json={"prompt": workflow},
)
response.raise_for_status()

result = response.json()
prompt_id = result["prompt_id"]
print(f"Job submitted: {prompt_id}")

Step 5. Monitor job progress

Jobs run asynchronously.
The docs cover polling and WebSocket.

Polling (simple)

Call GET /api/job/{prompt_id}/status on an interval.

statusMeaning
waiting_to_dispatchWaiting to dispatch
pendingQueued
in_progressRunning
success / completedSuccess (live API often returns success; Quick Start examples say completed)
error / failedFailed
cancelledCancelled

My Postman response matched the OpenAPI JobStatusResponse on success:

{
  "id": "1db59ce7-330d-4218-b889-ce92cbd63e5d",
  "status": "success",
  "created_at": "2026-07-15T10:10:35.257831Z",
  "updated_at": "2026-07-15T10:10:41.014223Z",
  "last_state_update": "2026-07-15T10:10:41.014223Z",
  "assigned_inference": "10.4.35.14:8080",
  "error_message": null
}
Postman GET /api/job/.../status showing success

Important: the status API does not include filenames.
It’s enough to know “done,” but you still need the next step for filename.

curl -X GET "$BASE_URL/api/job/$PROMPT_ID/status" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

Python wait loop (also accepts live success):

import time

def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> None:
    start_time = time.time()

    while time.time() - start_time < timeout:
        status_res = requests.get(
            f"{BASE_URL}/api/job/{prompt_id}/status",
            headers=get_headers(),
        )
        status_res.raise_for_status()
        status = status_res.json()["status"]

        if status in ("success", "completed"):
            return
        if 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 (realtime)

Prefer WebSocket if you want progress bars or per-node logs.

wss://cloud.comfy.org/ws?clientId={uuid}&token={api_key}

You’ll get messages like executing / progress / executed / execution_success — filter by prompt_id.
See the Cloud API Overview WebSocket section.

My first message in Postman looked like:

{
  "type": "status",
  "data": {
    "status": {
      "exec_info": {
        "queue_remaining": 0
      }
    },
    "sid": "31d0726d-635d-4a94-aaf5-1c862db38bbc"
  }
}

WebSocket is great for queue and progress.
It doesn’t reliably surface the final image filename for download, though.
For artifacts, Step 6’s job-details API was clearer for me.


Step 6. Download the output

You need filename (and maybe subfolder / type).
After the job finishes, get those from the job details API.

Get outputs and filenames

Endpoint: GET /api/jobs/{prompt_id} (Get full job details).
Note the path differs from /api/job/.../status (job singular vs jobs plural).

curl -X GET "$BASE_URL/api/jobs/$PROMPT_ID" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY"

In Postman:

  1. Method GET
  2. URL https://cloud.comfy.org/api/jobs/{prompt_id}
  3. Same X-API-Key auth
  4. Send

My response looked roughly like this (long workflow / execution_status.messages omitted):

{
  "id": "1db59ce7-330d-4218-b889-ce92cbd63e5d",
  "status": "completed",
  "outputs_count": 1,
  "execution_status": {
    "completed": true,
    "status_str": "success"
  },
  "outputs": {
    "9": {
      "images": [
        {
          "display_name": "test_api_00001_.png",
          "filename": "7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png",
          "subfolder": "",
          "type": "output"
        }
      ]
    }
  },
  "preview_output": {
    "filename": "7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png",
    "mediaType": "images",
    "nodeId": "9",
    "subfolder": "",
    "type": "output"
  }
}
Postman job details showing outputs (filename / display_name)

outputs is what you need for download:

FieldExampleUse
filenameLong hash-like .pngPass to /api/view (Cloud content-addressed name)
display_nametest_api_00001_.pngHuman-friendly name (from SaveImage filename_prefix)
subfolder""Empty unless you set one
typeoutputAlso passed to /api/view

Always use filename (the hash) with /api/view. display_name won’t work.

job_res = requests.get(
    f"{BASE_URL}/api/jobs/{prompt_id}",
    headers=get_headers(),
)
job_res.raise_for_status()
outputs = job_res.json()["outputs"]

for node_id, node_outputs in outputs.items():
    for file_info in node_outputs.get("images", []):
        print(
            node_id,
            file_info["filename"],
            file_info.get("display_name"),
            file_info.get("type", "output"),
        )

If you’re on WebSocket, executed message output can also carry file info.
In Postman I copied filename from job details, then downloaded.

Download the file bytes

Use GET /api/view.

# Use the hash filename from outputs
curl -L "$BASE_URL/api/view?filename=7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png&subfolder=&type=output" \
  -H "X-API-Key: $COMFY_CLOUD_API_KEY" \
  -o test_api_00001_.png

Saving as display_name (here test_api_00001_.png) keeps local files readable.

In Postman, set Params filename (hash) / subfolder / type, GET, then Save Response → Save to a file.
An image preview means it worked.

Postman /api/view image preview

Python batch save:

for node_outputs in outputs.values():
    for file_info in node_outputs.get("images", []):
        params = {
            "filename": file_info["filename"],  # hash side
            "subfolder": file_info.get("subfolder", ""),
            "type": file_info.get("type", "output"),
        }
        view_res = requests.get(
            f"{BASE_URL}/api/view",
            headers=get_headers(),
            params=params,
        )
        view_res.raise_for_status()

        # Save locally as display_name when present
        save_as = file_info.get("display_name") or file_info["filename"]
        with open(save_as, "wb") as f:
            f.write(view_res.content)
        print(f"Downloaded: {save_as}")

After an API run, the same result shows up in the browser workflow UI.
With SaveImage prefix test_api, the sidebar listed test_api_00001_.

Browser SaveImage after API run

If the PNG matches what you’d get from clicking Run in the UI, the full API path is working.


All-in-one Python sample

Putting the steps together (based on the official Complete Example):

import os
import json
import time
import requests

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 main():
    # 1. Load workflow and rewrite inputs
    with open("test-api.json") as f:
        workflow = json.load(f)

    workflow["57:3"]["inputs"]["seed"] = 42
    workflow["57:27"]["inputs"]["text"] = "a beautiful sunset"

    # 2. Submit job
    response = requests.post(
        f"{BASE_URL}/api/prompt",
        headers=get_headers(),
        json={"prompt": workflow},
    )
    response.raise_for_status()
    prompt_id = response.json()["prompt_id"]
    print(f"Job submitted: {prompt_id}")

    # 3. Poll until done
    while True:
        status_res = requests.get(
            f"{BASE_URL}/api/job/{prompt_id}/status",
            headers=get_headers(),
        )
        status_res.raise_for_status()
        status = status_res.json()["status"]

        if status in ("success", "completed"):
            break
        if status in ("error", "failed", "cancelled"):
            raise RuntimeError(f"Job {status}")
        time.sleep(2)

    # 4. Fetch outputs
    job_res = requests.get(
        f"{BASE_URL}/api/jobs/{prompt_id}",
        headers=get_headers(),
    )
    job_res.raise_for_status()
    outputs = job_res.json()["outputs"]

    # 5. Save files
    for node_outputs in outputs.values():
        for file_info in node_outputs.get("images", []):
            params = {
                "filename": file_info["filename"],
                "subfolder": file_info.get("subfolder", ""),
                "type": "output",
            }
            view_res = requests.get(
                f"{BASE_URL}/api/view",
                headers=get_headers(),
                params=params,
            )
            view_res.raise_for_status()
            save_as = file_info.get("display_name") or file_info["filename"]
            with open(save_as, "wb") as f:
                f.write(view_res.content)
            print(f"Downloaded: {save_as}")

if __name__ == "__main__":
    main()

If you already automate local ComfyUI, this API’s strength is that you can often swap base URL + API key and reuse a lot of the same script.


Takeaways from trying it

  • A workflow that runs in the browser ports to API cleanly (Export Workflow API does the heavy lifting)
  • Input swaps are just editing JSON inputs — nice for batching and external services
  • For first connectivity, hit /api/user/api/prompt in Postman before writing code
  • In job details outputs, filename is the hash, display_name is the readable name — pass the hash to /api/view
  • Free can’t run the API, so you need a paid plan to verify
  • It’s labeled Experimental — worth rechecking the docs now and then

Parallel job limits depend on plan (Standard 1 / Creator 3 / Pro 5).
For flooding multiple prompts, see the official parallel sample.


If you haven’t tried Comfy Cloud yet:

👉 Sign up for Comfy Cloud (invite link)

For real API use you need Standard or above, plus an API key from platform.comfy.org.


References


Wrap-up

Official docs are solid and clear.
Screenshots are a bit sparse though, so I hope this helps as a first studio-style walkthrough.
Before you automate or mass-produce, start by generating one image through the API.