{"slug": "comfyui-api-tutorial-2026-automate-image-generation", "title": "ComfyUI API Tutorial 2026: Automate Image Generation", "summary": "ComfyUI's built-in HTTP server on port 8188 accepts workflow JSON in API format, enabling image generation automation without the GUI. Developers can queue prompts, poll for completion, and download results using under 50 lines of Python. The API format strips GUI metadata, using numeric node IDs and connection arrays for efficient pipeline integration.", "body_md": "This article was originally published on\n\n[aifoss.dev]\n\n**TL;DR**: ComfyUI's built-in HTTP server accepts workflow JSON on port 8188 — the same JSON the GUI exports when you click \"Save (API Format)\". You can queue a prompt, poll for completion, and download the result in under 50 lines of Python. No GUI, no browser tab, no manual clicking.\n\nWhat you'll have running after this guide:\n\nThe GUI is fine for building and testing workflows. It's a problem once you need more than a few images, or need to integrate generation into a pipeline.\n\nCommon use cases where the API makes sense:\n\nIf you're generating one image at a time and want to tweak nodes visually, stay in the GUI. The API is for automation.\n\n`pip install requests websocket-client`\n\nIf you haven't installed ComfyUI yet, the [ComfyUI review](https://dev.to/blog/comfyui-review-2026/) covers installation from scratch. For GPU hardware requirements, the [Stable Diffusion 8GB VRAM guide](https://dev.to/blog/stable-diffusion-8gb-vram-guide-2026/) has a good breakdown of what each model tier needs.\n\nBy default, ComfyUI only listens on `127.0.0.1`\n\n— accessible only from the same machine. That's fine if you're scripting locally. For a remote GPU box or Docker container, add `--listen`\n\n:\n\n```\n# Local access only (same machine)\npython main.py --port 8188\n\n# All interfaces (for remote scripting or Docker)\npython main.py --listen 0.0.0.0 --port 8188\n\n# With VRAM optimization for 8–12 GB cards\npython main.py --listen 0.0.0.0 --port 8188 --lowvram\n```\n\nOnce running, verify it's up:\n\n```\ncurl http://127.0.0.1:8188/system_stats\n# {\"system\": {\"os\": \"posix\", \"python_version\": \"3.10.x\", ...}}\n```\n\nThe server starts a REST API and a WebSocket server on the same port. There is no authentication by default — if you expose this to a network, use a firewall rule or reverse proxy with auth.\n\nThe GUI workflow JSON and the API workflow JSON are different formats. The GUI format includes node positions, colors, and UI state. The API format is stripped down to just inputs and connections — which is all the server needs.\n\nTo export:\n\n`workflow_api.json`\n\nThe API format uses numeric string keys (`\"1\"`\n\n, `\"2\"`\n\n, `\"3\"`\n\n) as node IDs. Each node has a `class_type`\n\nand an `inputs`\n\nobject. Connections between nodes are expressed as `[\"source_node_id\", output_index]`\n\narrays rather than named references.\n\nHere is a minimal SDXL baseline workflow in API format:\n\n```\n{\n  \"4\": {\n    \"class_type\": \"CheckpointLoaderSimple\",\n    \"inputs\": {\n      \"ckpt_name\": \"sd_xl_base_1.0.safetensors\"\n    }\n  },\n  \"5\": {\n    \"class_type\": \"EmptyLatentImage\",\n    \"inputs\": {\n      \"batch_size\": 1,\n      \"height\": 1024,\n      \"width\": 1024\n    }\n  },\n  \"6\": {\n    \"class_type\": \"CLIPTextEncode\",\n    \"inputs\": {\n      \"clip\": [\"4\", 1],\n      \"text\": \"a photorealistic red fox in snow, golden hour lighting\"\n    }\n  },\n  \"7\": {\n    \"class_type\": \"CLIPTextEncode\",\n    \"inputs\": {\n      \"clip\": [\"4\", 1],\n      \"text\": \"blurry, low quality, watermark, text\"\n    }\n  },\n  \"3\": {\n    \"class_type\": \"KSampler\",\n    \"inputs\": {\n      \"cfg\": 7.0,\n      \"denoise\": 1.0,\n      \"latent_image\": [\"5\", 0],\n      \"model\": [\"4\", 0],\n      \"negative\": [\"7\", 0],\n      \"positive\": [\"6\", 0],\n      \"sampler_name\": \"euler\",\n      \"scheduler\": \"normal\",\n      \"seed\": 42,\n      \"steps\": 20\n    }\n  },\n  \"8\": {\n    \"class_type\": \"VAEDecode\",\n    \"inputs\": {\n      \"samples\": [\"3\", 0],\n      \"vae\": [\"4\", 2]\n    }\n  },\n  \"9\": {\n    \"class_type\": \"SaveImage\",\n    \"inputs\": {\n      \"filename_prefix\": \"api_output\",\n      \"images\": [\"8\", 0]\n    }\n  }\n}\n```\n\nFor SDXL specifically, node `\"4\"`\n\nconnects to both CLIP encoders and the KSampler via its three outputs: `[0]`\n\n= model, `[1]`\n\n= CLIP, `[2]`\n\n= VAE. The connection syntax `[\"4\", 1]`\n\nmeans \"output index 1 of node 4.\"\n\n``` python\nimport uuid\nimport json\nimport requests\n\nSERVER = \"127.0.0.1:8188\"\nCLIENT_ID = str(uuid.uuid4())\n\ndef queue_prompt(workflow: dict) -> str:\n    \"\"\"Submit a workflow. Returns the prompt_id for tracking.\"\"\"\n    payload = {\"prompt\": workflow, \"client_id\": CLIENT_ID}\n    r = requests.post(f\"http://{SERVER}/prompt\", json=payload)\n    r.raise_for_status()\n    return r.json()[\"prompt_id\"]\n```\n\nThe `client_id`\n\nis a UUID you generate once per session. It ties your WebSocket connection to your HTTP requests so the server routes status messages back to you. You can skip it for pure HTTP polling, but you need it for WebSocket tracking.\n\nThe server responds immediately with `{\"prompt_id\": \"<uuid>\", \"number\": <queue_position>}`\n\n. Generation hasn't started yet — the prompt is in queue.\n\nTwo approaches — choose based on your use case:\n\n| Approach | Latency | Complexity | Best for |\n|---|---|---|---|\nHTTP polling `/history/{id}`\n|\n~1s overhead | Low — no extra library | Scripts, batch jobs |\nWebSocket `/ws?clientId=...`\n|\nNear-real-time | Medium — event loop | Apps that show progress |\n\n``` php\nimport time\n\ndef wait_for_completion(prompt_id: str, poll_secs: float = 1.0) -> dict:\n    \"\"\"Block until the prompt finishes. Returns the history entry.\"\"\"\n    url = f\"http://{SERVER}/history/{prompt_id}\"\n    while True:\n        r = requests.get(url)\n        r.raise_for_status()\n        history = r.json()\n        if prompt_id in history:\n            return history[prompt_id]\n        time.sleep(poll_secs)\n```\n\nThe `/history/{prompt_id}`\n\nendpoint returns an empty dict `{}`\n\nwhile the prompt is queued or running, and the full result object once done. Polling every second adds at most 1 second of latency to your total generation time — acceptable for batch scripts.\n\n``` php\nimport websocket\n\ndef generate_with_ws(workflow: dict) -> str:\n    \"\"\"Queue and wait for completion via WebSocket. Returns prompt_id.\"\"\"\n    ws = websocket.WebSocket()\n    ws.connect(f\"ws://{SERVER}/ws?clientId={CLIENT_ID}\")\n    prompt_id = queue_prompt(workflow)\n\n    while True:\n        raw = ws.recv()\n        if not isinstance(raw, str):\n            continue  # binary preview frames — skip\n        msg = json.loads(raw)\n        if msg[\"type\"] == \"executing\":\n            data = msg[\"data\"]\n            if data[\"node\"] is None and data[\"prompt_id\"] == prompt_id:\n                break  # null node = generation finished\n\n    ws.close()\n    return prompt_id\n```\n\nThe `executing`\n\nmessage fires for each node as it runs. When `node`\n\nis `None`\n\nand the `prompt_id`\n\nmatches yours, the entire graph has finished executing.\n\n``` python\npython\nimport os\n\ndef download_images(history_entry: dict, output_dir: str = \"output\") -> list[str]:\n    \"\"\"Download all output images from a completed prompt.\"\"\"\n    os.makedirs(output_dir, exist_ok=True)\n    saved = []\n```\n\n", "url": "https://wpnews.pro/news/comfyui-api-tutorial-2026-automate-image-generation", "canonical_source": "https://dev.to/jovan_chan_9500711396d4e6/comfyui-api-tutorial-2026-automate-image-generation-3j2j", "published_at": "2026-06-14 07:01:04+00:00", "updated_at": "2026-06-14 07:28:53.682758+00:00", "lang": "en", "topics": ["developer-tools", "artificial-intelligence", "generative-ai", "large-language-models", "ai-tools"], "entities": ["ComfyUI", "Python", "Stable Diffusion", "SDXL"], "alternates": {"html": "https://wpnews.pro/news/comfyui-api-tutorial-2026-automate-image-generation", "markdown": "https://wpnews.pro/news/comfyui-api-tutorial-2026-automate-image-generation.md", "text": "https://wpnews.pro/news/comfyui-api-tutorial-2026-automate-image-generation.txt", "jsonld": "https://wpnews.pro/news/comfyui-api-tutorial-2026-automate-image-generation.jsonld"}}