If you are wiring ComfyUI into an automation pipeline, the first thing most people reach for is a webhook: submit the prompt, give it a callback URL, and get notified when the image is ready. That assumption is the source of a lot of wasted debugging, because ComfyUI does not expose a webhook endpoint. There is no POST /prompt callback URL, no notify_url field, and no built-in outbound HTTP hook in the current server.
This guide is based on reading the ComfyUI server and execution source directly, not on a particular UI version. The automation path that actually works has three reliable pieces: the WebSocket event stream at /ws, the per-prompt history endpoint at /history/{prompt_id}, and the queue state endpoint at /queue. Understand all three and you can build notifications that are at least as reliable as a webhook, without waiting for one to be added.
Why There Is No Webhook (And What To Use Instead)
ComfyUI’s HTTP server (server.py) exposes a fixed set of routes. The prompt submission route is POST /prompt, and it returns immediately with a prompt_id plus a number (the queue position). It does not accept a callback URL, and nothing in the request body causes the server to make an outbound request later. Job completion is communicated only two ways: over the in-band WebSocket channel, or by polling the history endpoints.
This is a deliberate design. A webhook would require ComfyUI to hold outbound network state, handle retries, and trust caller-supplied URLs — all friction for a local-first tool where the client is usually on the same machine. The result is that you own the notification. The good news: everything you need to build one is already there.
Note that the newer /api/jobs endpoints (visible in the route table) expose job objects with lifecycle state, but for image-generation automation the three endpoints above remain the practical, stable surface. Different ComfyUI frontends and third-party runners agree on them precisely because they are the lowest common denominator.
The WebSocket Stream: The Event-Based Replacement for a Webhook
The closest thing to a push-based callback is the WebSocket at GET /ws. It accepts an optional clientId query parameter; if you omit it, the server assigns one and returns it in the initial status message. Reuse that clientId across reconnects so the server keeps you bound to the same logical session.
When you submit a prompt, you can pass extra_data.client_id in the JSON body. The server folds that value into the queued item, and the execution engine uses the socket bound to that id to stream progress. The message types you care about are:
executing— sent as each node starts, with the node idexecuted— sent per node as it finishes, including the node’s outputexecution_success— sent once when the whole prompt finishes, carrying theprompt_idexecution_error/execution_interrupted— the failure signalsprogress— per-step sampler progress when a progress handler is active
A minimal polling-free listener looks like this (Python, using the websockets library):
import json, asyncio, websockets
async def listen(client_id):
uri = f"ws://127.0.0.1:8188/ws?clientId={client_id}"
async with websockets.connect(uri) as ws:
while True:
msg = json.loads(await ws.recv())
if msg["type"] == "execution_success":
print("DONE", msg["data"]["prompt_id"])
return
if msg["type"] in ("execution_error", "execution_interrupted"):
print("FAILED", msg)
return
The key operational detail: the event is tied to your client_id. If you open the socket without passing the id you submitted with, or if you submit without extra_data.client_id and listen on a fresh socket, you will not reliably receive the completion signal. Align the two ids and this behaves like a push notification.
Polling /history/{prompt_id}: The Durable, Restart-Safe Fallback
WebSocket messages are ephemeral. If your listener process dies, sleeps, or reconnects at the wrong moment, the execution_success frame is gone and you have no record. For long-running or unattended pipelines, poll GET /history/{prompt_id} instead. It returns a durable record keyed by prompt id:
{
"1234": {
"prompt": [...],
"outputs": {
"9": {"images": [{"filename": "ComfyUI_00042_.png", ...}]}
},
"status": {"status_str": "success", "completed": true}
}
}
Poll it on an interval (a few seconds is plenty for image generation), and treat status.status_str as the source of truth: success means the outputs in that same object are final. This pattern survives a client restart, a network blip, and even a ComfyUI restart (history is kept in memory, so a full server restart clears it — see the caveats below).
Inside the output object, the images array gives you filenames under the ComfyUI output directory. You can fetch them over GET /view?filename=... or read them straight off disk when running locally.
Reading /queue To Decide When To Submit
Blindly submitting prompts is how you end up with a backlog. GET /queue returns queue_running and queue_pending, so you can gate submissions:
import requests
q = requests.get("http://127.0.0.1:8188/queue").json()
ahead = len(q["queue_running"]) + len(q["queue_pending"])
print(f"{ahead} items ahead of a new submission")
This matters most when multiple workers or a web frontend share one ComfyUI instance. queue_running is a single item (the currently executing prompt), while queue_pending is the waiting list. If a job is already in flight, your new prompt will simply queue behind it — the number returned by POST /prompt tells you exactly how far back you are.
Common Pitfalls When Automating
Several failures come up repeatedly and are almost never ComfyUI bugs:
- client_id mismatch. The WebSocket received
execution_successfor nothing because the listener’s id differs from the submitter’s id. Passextra_data.client_idexplicitly, and reuse it. - Assuming JSON output is a URL. History gives you a filename, not a public URL. You must resolve it via
/viewor the filesystem; a webhook-style consumer that expects a URL will silently break. - Reliance on in-memory history.
/historyis not persisted to disk. Restart the ComfyUI process and prior records vanish. If you need durable job tracking, persist prompt ids and outputs yourself. - Ignoring
execution_cached. When a node’s inputs are unchanged, ComfyUI may return cached outputs and emitexecution_cachedrather than re-running it. Your listener should treat cached completion the same as fresh completion or you will stall waiting for anexecutedframe that never comes. - Polling too aggressively. History-polling every 100 ms just adds load to a local server. A 2–5 second interval is enough and far friendlier to the aiohttp event loop.
Verifying Your Automation End To End
Build a tiny harness that strings the three pieces together and confirm each layer independently before trusting a big pipeline:
- Submit a trivial no-op or tiny workflow and capture the returned
prompt_id. - Queue check: confirm
/queueshowsqueue_runningpopulated while it executes. - History check: poll
/history/{prompt_id}untilstatus.status_str == "success", then read the output filenames. - Socket check: with the same
client_id, captureexecution_successand confirm it carries the matchingprompt_id.
Only after all four pass should you scale up to real workflows. The WebSocket path is what you want for low-latency, interactive-looking notifications; the history-polling path is what you want for unattended batch jobs that must survive a listener restart. Used together — WebSocket as the fast signal, history as the durable record — they cover everything a webhook would, without depending on a feature that does not exist.
Conclusion
Stop looking for a ComfyUI webhook: there is not one, and the maintainers’ local-first design makes one unlikely to appear in the form people expect. Instead, treat the WebSocket /ws event stream as your push channel, /history/{prompt_id} as your durable source of truth, and /queue as your backpressure gauge. Align the client_id across submit and listen, resolve filenames yourself, and persist prompt ids if you need records to survive a restart. That combination gives you reliable, auditable automation on top of the API ComfyUI actually ships.
For the broader API automation harness that handles submission, history, and output download in one place, see our guide on building a reliable Python harness for ComfyUI. If you are running video models on a local GPU and want the resource picture before automating, the Wan2.2 Animate 14B BlockSwap walkthrough walks through VRAM constraints that affect queue timing. For the authoritative API surface, refer to the official ComfyUI endpoints documentation and the ComfyUI server source, which is the ground truth when UI behavior and documentation disagree.
Leave a Reply