Tutorial · Beginner
ComfyUI API Basics: driving workflows programmatically
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:
- Batch processing – generate hundreds of variants automatically.
- Integration – image generation from your own app or backend.
- Pipelines/CI – generate assets reproducibly via script.
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:
# 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
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:
- Menu:
Workflow → Export (API)or, in older versions,File → Export Workflow (API)downloads a.jsonfile with just the API data. - If the menu item is missing: enable the Dev Mode Options in the settings (the gear icon) – then the "Save (API Format)" button appears.
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]:
{
"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] }
}
}
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.
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:
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).
# 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):
{
"<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
| Problem | Cause & solution |
|---|---|
| Port 8188 in use | Choose 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 found | Match the filenames in the JSON against models/checkpoints/ |
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.
Did this tutorial help you?
This content is free. I'd really appreciate a coffee. ☕