API do ComfyUI com Python: fluxos de trabalho, endpoints e exemplos
O ComfyUI já roda um servidor HTTP, então o Python pode controlá-lo de ponta a ponta. Exporte um fluxo de trabalho no formato API, enfileire-o, acompanhe o progresso por WebSocket, baixe as imagens e evite os erros que quebram scripts sem supervisão. Inclui uma classe cliente reutilizável e um loop de lote.
O ComfyUI parece uma prancheta para grafos de nós, mas, por baixo da tela, é um servidor HTTP comum. Cada botão que você aperta no navegador chama um endpoint, e um script Python pode chamar exatamente os mesmos endpoints. Essa é a ideia por trás da API do ComfyUI com Python: exportar um fluxo de trabalho como JSON, mudar dois ou três valores, enviar com POST para /prompt e depois coletar as imagens prontas. Sem aba do navegador, sem cliques, sem supervisão. Este artigo mostra os endpoints reais, um listener WebSocket para o progresso ao vivo e exemplos funcionais que você pode colar em um arquivo e executar na sua máquina.
Por que automatizar o ComfyUI
Fazer uma imagem à mão é tranquilo. Fazer duzentas fotos de produto, rodar um job noturno de miniaturas ou deixar os clientes apertarem um botão no seu próprio app é outra história, e a tela não faz nada disso. Um servidor ComfyUI headless (sem interface) consegue, e o Python é o caminho mais curto até ele. Como bônus, seus prompts, seeds e configurações acabam em um repositório Git, em vez de em uma pasta cheia de capturas de tela.
A API se justifica em três situações:
Trabalho em lote: centenas de prompts, um único template, zero cliques manuais.
Produtos: seu próprio app envia uma requisição e recebe uma imagem de volta.
Automação: um cron job, um chatbot ou uma etapa de CI que gera recursos em um cronograma.
O que o servidor expõe
Inicie o ComfyUI do jeito habitual (python main.py) e ele escuta em 127.0.0.1:8188. Adicione --listen 0.0.0.0 para aceitar conexões de outras máquinas e --port para mudar a porta. Estes são os endpoints que você mais vai usar:
Método
Endpoint
O que faz
POST
/prompt
Enfileira um fluxo de trabalho e retorna um prompt_id
GET
/history/{prompt_id}
Retorna as saídas e o status quando a execução termina
GET
/view
Baixa uma imagem por filename, subfolder e type
POST
/upload/image
Coloca uma imagem na pasta de entrada do ComfyUI
GET
/queue
Lista os prompts em execução e pendentes
POST
/interrupt
Interrompe o prompt que está rodando agora
GET
/object_info
Descreve cada classe de nó e suas entradas
GET
/system_stats
Informa os detalhes de VRAM, RAM e dispositivo
WebSocket
/ws?clientId=...
Transmite eventos ao vivo para o seu cliente
O ritmo nunca muda: enfileirar, esperar, buscar. Você envia um grafo com POST, espera (consultando periodicamente ou escutando o WebSocket) e baixa o que o grafo produziu.
Exporte seu fluxo de trabalho no formato API
Primeiro monte e teste o grafo na tela. Quando ele gerar a imagem que você quer, exporte. Seu script não consegue rodar o arquivo de fluxo normal, porque esse formato guarda posições dos nós, cores e o layout dos widgets. O script precisa da versão enxuta, em que cada nó é reduzido à sua classe e às suas entradas.
Formato API ou JSON normal
Nas versões atuais da interface, abra o menu Workflow e escolha Exportar (API). Nas versões mais antigas, ative Dev mode options nas configurações e use o botão Save (API Format). Salve o resultado como workflow_api.json ao lado do seu script.
💡 Dica: Guarde os dois arquivos. O JSON normal reabre na tela para edição, enquanto o JSON de API é o que o seu código envia.
Anatomia do JSON exportado
Abra o arquivo e você verá um dicionário simples. Cada entrada leva o nome de um ID de nó (uma string), e o valor traz um class_type mais as inputs do nó:
Este exemplo supõe um checkpoint Flux Dev, por isso cfg está em 1.0. Checkpoints no estilo Stable Diffusion 3.5 Large costumam pedir um valor maior, muitas vezes entre 4 e 8, então copie os números do seu próprio export, e não de um tutorial.
Dois detalhes importam. Valores simples, como seed e steps, são os controles que você muda pelo Python. Valores como ["4", 0] são links: o primeiro item é o ID do nó de origem, e o segundo é o slot de saída que será lido. Não mexa nos links, a menos que esteja reconectando o grafo de propósito.
💡 Dica: Renomeie seus nós de prompt na tela ("Positive Prompt", "Negative Prompt") antes de exportar. O nome vai para _meta.title, e seu código pode encontrar os nós pelo título, em vez de por um número frágil.
Sua primeira chamada em Python
Dois pacotes bastam para tudo neste artigo: pip install requests websocket-client. Salve o export como workflow_api.json, inicie o ComfyUI e execute os trechos na ordem.
Instale e enfileire um prompt
import json
import requests
SERVER = "http://127.0.0.1:8188"
with open("workflow_api.json", "r", encoding="utf-8") as f:
workflow = json.load(f)
# "6" is the positive CLIPTextEncode, "3" is the KSampler
workflow["6"]["inputs"]["text"] = "a lighthouse at dawn, 35mm photo, film grain"
workflow["3"]["inputs"]["seed"] = 421337
response = requests.post(f"{SERVER}/prompt", json={"prompt": workflow})
response.raise_for_status()
prompt_id = response.json()["prompt_id"]
print("Queued:", prompt_id)
O ComfyUI responde com um corpo JSON contendo prompt_id e number, a posição na fila. Nada foi renderizado até aqui. O prompt apenas foi aceito. Guarde o ID, porque todas as chamadas seguintes precisam dele.
Consulte o endpoint de histórico
A forma mais simples de saber que um prompt terminou é consultar o endpoint de histórico até ele responder. Enquanto a execução ainda estiver em andamento, /history/{prompt_id} retorna um objeto vazio.
import time
def wait_for_outputs(prompt_id, timeout=300):
started = time.time()
while time.time() - started < timeout:
history = requests.get(f"{SERVER}/history/{prompt_id}").json()
if prompt_id in history:
return history[prompt_id]["outputs"]
time.sleep(1)
raise TimeoutError(f"Prompt {prompt_id} took longer than {timeout}s")
O dicionário outputs é indexado pelo ID do nó. Cada nó SaveImage informa uma lista chamada images, e cada imagem é um pequeno dicionário com um filename, um subfolder e um type.
Baixe a imagem pronta
import os
def download_images(outputs, folder="renders"):
os.makedirs(folder, exist_ok=True)
saved = []
for node_id, node_output in outputs.items():
for image in node_output.get("images", []):
if image["type"] != "output":
continue # skip PreviewImage temp files
data = requests.get(f"{SERVER}/view", params=image).content
path = os.path.join(folder, image["filename"])
with open(path, "wb") as f:
f.write(data)
saved.append(path)
return saved
print(download_images(wait_for_outputs(prompt_id)))
O dicionário da imagem já tem os três parâmetros que /view espera, então pode ser passado diretamente como string de consulta. Os nós SaveImage informam type: "output", enquanto os nós PreviewImage informam temp, e por isso o loop filtra por esse campo.
Progresso ao vivo via WebSocket
A consulta periódica funciona, mas desperdiça chamadas e não diz nada até o fim. O ComfyUI também fala WebSocket, o que oferece um fluxo ao vivo: mudanças na fila, o nó em execução e um contador para cada passo do amostrador. Em um app web, é isso que alimenta a barra de progresso.
Conecte com um client ID
Gere um UUID uma vez e use-o em dois lugares: na string de consulta clientId do socket e no campo client_id da sua requisição /prompt. O ComfyUI envia os eventos de um prompt apenas ao cliente que o enfileirou. Se os IDs não baterem, seu socket fica em silêncio.
import json
import uuid
import requests
import websocket # pip install websocket-client
HOST = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())
def run_with_progress(workflow):
ws = websocket.WebSocket()
ws.connect(f"ws://{HOST}/ws?clientId={CLIENT_ID}")
payload = {"prompt": workflow, "client_id": CLIENT_ID}
r = requests.post(f"http://{HOST}/prompt", json=payload)
r.raise_for_status()
prompt_id = r.json()["prompt_id"]
while True:
message = ws.recv()
if isinstance(message, bytes):
continue # binary frames are preview thumbnails
event = json.loads(message)
kind, data = event["type"], event["data"]
if kind == "progress":
print(f"step {data['value']}/{data['max']}")
elif kind == "execution_error":
raise RuntimeError(data.get("exception_message", "node failed"))
elif data.get("prompt_id") == prompt_id and (
kind == "execution_success"
or (kind == "executing" and data["node"] is None)
):
break
ws.close()
history = requests.get(f"http://{HOST}/history/{prompt_id}").json()
return history[prompt_id]["outputs"]
Os frames binários carregam miniaturas de prévia enquanto o amostrador trabalha, então o loop ignora tudo que não for texto. Se você quiser prévias ao vivo na sua própria interface, decodifique esses frames em vez de ignorá-los.
Mensagens que você vai receber
Tipo de mensagem
Significado
status
O tamanho da fila mudou
execution_start
Seu prompt saiu da fila e começou a rodar
execution_cached
Lista os nós ignorados porque o resultado estava em cache
executing
O nó em execução agora; node: null significa que o grafo terminou
progress
Passo value de max do amostrador
executed
Um nó produziu uma saída, como nomes de arquivos salvos
execution_error
Um nó gerou uma exceção
execution_success
O prompt inteiro foi concluído com sucesso (versões mais recentes)
A mensagem executing com node igual a null é o sinal clássico de fim de execução. As versões mais recentes acrescentam execution_success, e tratar os dois casos mantém seu script funcionando entre versões.
Encapsule em uma classe cliente
Funções soltas servem para um primeiro teste. Qualquer coisa que rode mais de uma vez merece uma pequena classe que guarde o host, o client ID e as operações que você repete.
import time
import uuid
import requests
class ComfyClient:
def __init__(self, host="127.0.0.1:8188"):
self.host = host
self.client_id = str(uuid.uuid4())
def queue(self, workflow):
r = requests.post(
f"http://{self.host}/prompt",
json={"prompt": workflow, "client_id": self.client_id},
)
if r.status_code != 200:
raise RuntimeError(r.text) # includes node_errors
return r.json()["prompt_id"]
def result(self, prompt_id, timeout=300):
deadline = time.time() + timeout
while time.time() < deadline:
history = requests.get(f"http://{self.host}/history/{prompt_id}").json()
if prompt_id in history:
return history[prompt_id]
time.sleep(1)
raise TimeoutError(prompt_id)
def fetch(self, image):
r = requests.get(f"http://{self.host}/view", params=image)
r.raise_for_status()
return r.content
def upload(self, path):
with open(path, "rb") as f:
r = requests.post(
f"http://{self.host}/upload/image",
files={"image": f},
data={"overwrite": "true"},
)
r.raise_for_status()
return r.json()["name"]
Troque prompts e seeds com segurança
Nunca fixe IDs de nós como "6" em um projeto real. Se você exportar o grafo de novo, os números podem mudar. Procure os nós pela classe e pelo título, e sempre edite uma cópia do template, para que um job não contamine o seguinte.
import copy
import json
import random
def find_node(workflow, class_type, title=None):
for node_id, node in workflow.items():
if node["class_type"] != class_type:
continue
if title is None or node.get("_meta", {}).get("title") == title:
return node_id
raise LookupError(f"{class_type} {title or ''} not found")
def build(template, prompt, seed=None):
wf = copy.deepcopy(template)
wf[find_node(wf, "CLIPTextEncode", "Positive Prompt")]["inputs"]["text"] = prompt
wf[find_node(wf, "KSampler")]["inputs"]["seed"] = (
seed if seed is not None else random.randint(0, 2**32 - 1)
)
return wf
template = json.load(open("workflow_api.json", encoding="utf-8"))
client = ComfyClient()
prompts = [
"ceramic teapot on a linen cloth, soft window light",
"walnut desk with a fountain pen, low morning sun",
"leather boots on wet cobblestones, overcast sky",
]
ids = [client.queue(build(template, p)) for p in prompts] # queue everything first
for pid in ids:
entry = client.result(pid)
for out in entry["outputs"].values():
for image in out.get("images", []):
with open(image["filename"], "wb") as f:
f.write(client.fetch(image))
Enfileire tudo primeiro e depois colete. O ComfyUI executa os prompts um de cada vez, na ordem em que chegaram, então a GPU nunca fica ociosa enquanto seu script baixa um arquivo.
💡 Dica: Precisa de 200 prompts em vez de três? Peça a um modelo de linguagem como o Claude Sonnet 5 ou o Gemini 3.5 Flash que os escreva como uma lista JSON e alimente essa lista diretamente no loop acima.
Envie imagens para edições
Os grafos de imagem para imagem, inpainting e ControlNet começam com um nó LoadImage. Esse nó lê da pasta de entrada do ComfyUI, então envie o arquivo primeiro e aponte o nó para o nome retornado.
name = client.upload("portrait.png")
wf = copy.deepcopy(template)
wf[find_node(wf, "LoadImage")]["inputs"]["image"] = name
pid = client.queue(wf)
É aqui que a automação se aproxima do trabalho de efeitos visuais. Remoção de objetos, trocas de fundo e mudanças de iluminação seguem o mesmo loop: enviar uma imagem de origem, definir uma máscara e um prompt, enfileirar, buscar. Coloque isso em uma função, e uma pasta com 500 fotos vira um único comando.
Erros de produção a evitar
Scripts que funcionam no seu notebook quebram de formas previsíveis quando rodam sem supervisão. Três problemas explicam a maior parte das dúvidas de suporte.
Prompts em cache voltam na hora
O ComfyUI guarda em cache os resultados dos nós de acordo com as entradas. Se você enfileirar exatamente o mesmo grafo duas vezes, a segunda execução não roda nada, e a mesma imagem volta em milissegundos. A opção "randomize seed after each run" existe apenas na interface do navegador. O JSON de API tem um número fixo, então seu código precisa escolher um seed novo sempre que quiser uma imagem nova.
Leia node_errors direito
Quando a validação falha, /prompt responde com HTTP 400 e um corpo contendo error e node_errors. O segundo campo indica o ID exato do nó e a entrada com problema, por exemplo um nome de arquivo de checkpoint que não está instalado nesta máquina. Imprima o corpo inteiro, e não só o código de status. Lembre-se também de que um prompt pode passar pela validação e ainda falhar durante a execução, caso em que a entrada do histórico mostra status_str: "error".
Nunca exponha a porta 8188
O ComfyUI vem sem login. Quem conseguir acessar a porta pode enfileirar jobs, ler sua pasta de saída e chamar /object_info. Os nós personalizados são Python comum e rodam com as permissões do seu usuário. Associe o servidor a 127.0.0.1, ou coloque-o atrás de um proxy reverso com autenticação ou de uma VPN. Se um app web em outra origem precisar chamá-lo, passe --enable-cors-header com essa única origem, em vez de um curinga.
💡 Dica: Ficando sem VRAM depois de muitos checkpoints diferentes? Envie um POST para {"unload_models": true, "free_memory": true} em /free entre os lotes para liberar memória sem reiniciar o servidor.
Pule o servidor com o Picasso IA
Nem todo projeto precisa de uma máquina com GPU, de um ambiente Python e de uma fila para vigiar. Se o seu objetivo é simplesmente obter boas imagens a partir de textos ou fotos de referência, o Picasso IA roda modelos comparáveis no navegador. Veja como os modelos se encaixam nos trabalhos mais comuns do ComfyUI:
Escreva seu prompt. Nomeie o assunto, a luz e a lente, do mesmo jeito que faria em um nó de texto do ComfyUI.
Escolha uma proporção. O padrão é 1:1, 16:9 serve para banners, e match_input_image mantém a forma de uma foto enviada.
Escolha uma resolução. O padrão é 1 MP, e o modelo aceita até 4 MP, embora 2 MP ou menos seja o recomendado.
Adicione até 8 imagens de entrada se quiser que o resultado siga um estilo, um rosto ou uma foto de produto.
Defina o formato de saída (WebP, JPG ou PNG) e clique em gerar. Reutilize o seed depois para recriar o mesmo resultado.
Configuração
Padrão
Conselho prático
Resolução
1 MP
Fique em 2 MP ou menos para os melhores resultados
Qualidade de saída
80
Faixa de 0 a 100, ignorada para PNG
Tolerância de segurança
2
1 é o mais rígido, 5 é o mais permissivo
Seed
Aleatório
Fixe para reproduzir uma imagem exatamente
💡 Dica: Os hábitos que você construiu acima se transferem diretamente. Seeds fixos para resultados repetíveis, uma mudança por execução e prompts curtos que citem luz e lente funcionam igual nas duas plataformas.
Crie suas próprias imagens hoje
Agora você tem o ciclo completo: exportar o grafo, enfileirá-lo, escutar o socket, buscar os arquivos. Rode o primeiro trecho hoje à noite e você terá uma imagem salva no disco antes que o café esfrie. Depois, refine a classe cliente, adicione a lógica de seed e deixe um lote rodar enquanto faz outra coisa.
E se preferir pular a configuração, abra o Picasso IA, escolha o Flux Dev ou o Flux 2 Pro e digite o primeiro prompt que vier à cabeça. Mude uma configuração, gere de novo e compare. Cinco minutos de experimentos vão ensinar mais sobre prompts, seeds e proporções do que qualquer quantidade de leitura.