Work in progress: under construction · noindex

Tutorial · Beginner

ComfyUI API Basics: driving workflows programmatically

Python ComfyUI AI image generation Reading time ~10 min Updated: July 2026

ComfyUI is not just a graphical interface – every workflow can also be triggered via the API. With that you automate image generation, embed it into apps or produce entire series. This tutorial shows the complete path from starting the server to fetching the image.

Prerequisites & use cases

You should already be able to operate ComfyUI through the GUI and be familiar with a simple text-to-image workflow. You'll also need Python 3.9+ and the requests library (pip install requests).

Typical use cases for the API:

The API is an ordinary HTTP interface. If terms like POST or status codes don't mean anything to you yet, read the REST API basics first.

Starting the API server

The ComfyUI API runs in the same process as the GUI – you don't need to install anything extra. Start ComfyUI as usual:

bash
# Starts server + API at http://127.0.0.1:8188
python main.py

# Check whether the server responds:
curl http://127.0.0.1:8188/system_stats
Security

With --listen 0.0.0.0 the server is reachable across the whole network – ComfyUI has no built-in authentication. Use this only in trusted networks or behind a reverse proxy with access control.

Exporting the workflow in API format

Important: the API doesn't expect the normal workflow JSON from the browser, but the leaner API format (without layout info such as positions or colours). Here's how to get it:

The exported JSON is a collection of nodes. Each node has an ID, a class_type and its inputs. References to other nodes are given as a list ["node_id", output_index]:

json — workflow_api.json (excerpt)
{
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 42,
      "steps": 20,
      "cfg": 8,
      "sampler_name": "euler",
      "model": ["4", 0],
      "positive": ["6", 0],
      "latent_image": ["5", 0]
    }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "a red sports car", "clip": ["4", 1] }
  }
}
Note the node IDs

To change the prompt later via script, you need to know which node is your text prompt (here node "6", a CLIPTextEncode). You can see the IDs in the exported JSON.

Sending your first API call

A workflow is queued via POST /prompt. The body must have the form { "prompt": <workflow> }. The response returns a prompt_id.

python — generate.py
import requests
import json
import time

SERVER = "http://127.0.0.1:8188"

# 1. Load the workflow in API format
with open("workflow_api.json", "r", encoding="utf-8") as f:
    workflow = json.load(f)

# 2. Set the prompt text dynamically (node "6" = CLIPTextEncode)
workflow["6"]["inputs"]["text"] = "a red sports car, studio lighting"

# 3. Queue the workflow
resp = requests.post(f"{SERVER}/prompt", json={"prompt": workflow})
resp.raise_for_status()
prompt_id = resp.json()["prompt_id"]
print("Prompt-ID:", prompt_id)

As a curl variant, the body must likewise be wrapped in {"prompt": …}. The exported file, however, contains only the workflow itself – with jq you wrap it appropriately:

bash
jq '{prompt: .}' workflow_api.json \
  | curl -X POST http://127.0.0.1:8188/prompt \
      -H "Content-Type: application/json" \
      -d @-

Retrieving images

Generation runs asynchronously. You poll GET /history/{prompt_id} until your job appears there. You then fetch the finished images via GET /view – this endpoint needs three parameters: filename, subfolder and type (usually output).

python — generate.py (continued)
# 4. Wait for completion (polling the history)
while True:
    history = requests.get(f"{SERVER}/history/{prompt_id}").json()
    if prompt_id in history:
        outputs = history[prompt_id]["outputs"]
        break
    time.sleep(1)

# 5. Download the image(s)
for node_id, node_output in outputs.items():
    for image in node_output.get("images", []):
        params = {
            "filename": image["filename"],
            "subfolder": image["subfolder"],   # often empty ""
            "type": image["type"],             # usually "output"
        }
        data = requests.get(f"{SERVER}/view", params=params).content
        with open(image["filename"], "wb") as out:
            out.write(data)
        print("Gespeichert:", image["filename"])

The history response is structured like this (simplified):

json — response from /history/{prompt_id}
{
  "<prompt_id>": {
    "outputs": {
      "9": {
        "images": [
          { "filename": "ComfyUI_00001_.png", "subfolder": "", "type": "output" }
        ]
      }
    }
  }
}

More robust: WebSocket instead of polling

Polling works, but it's inefficient. Cleaner is the WebSocket endpoint ws://127.0.0.1:8188/ws?clientId=<uuid>: it reports progress and completion in real time. Also pass the same clientId in the /prompt body so you can map the events. The job is done when an executing message with node: null and the matching prompt_id arrives. For getting started the polling above is enough – for production setups it's worth switching to WebSocket.

Troubleshooting

ProblemCause & solution
Port 8188 in useChoose a different port: python main.py --port 8189
Browser access blocked (CORS)Start the server with --enable-cors-header
KeyError: 'prompt_id'Check node_errors in the response – usually the workflow JSON is invalid or a model is missing
Image not found (404 on /view)Pass subfolder and type from the history, not just filename
Model not foundMatch the filenames in the JSON against models/checkpoints/
In summary

Three endpoints are enough to get started: POST /prompt (trigger), GET /history/{prompt_id} (status) and GET /view (fetch the image). With those you can automate any GUI action – the foundation for batch generation and your own tools.