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.

API do ComfyUI com Python: fluxos de trabalho, endpoints e exemplos
Cristian Da Conceicao
Fundador do Picasso IA

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.

Mãos digitando código Python em um notebook ao lado de uma xícara de café

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.

Gabinete de PC aberto com uma placa de vídeo grande sobre uma bancada de madeira

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étodoEndpointO que faz
POST/promptEnfileira 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/viewBaixa uma imagem por filename, subfolder e type
POST/upload/imageColoca uma imagem na pasta de entrada do ComfyUI
GET/queueLista os prompts em execução e pendentes
POST/interruptInterrompe o prompt que está rodando agora
GET/object_infoDescreve cada classe de nó e suas entradas
GET/system_statsInforma 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.

Engenheiro estudando um diagrama de nós em um monitor grande de escritório

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ó:

{
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 421337,
      "steps": 20,
      "cfg": 1.0,
      "sampler_name": "euler",
      "scheduler": "simple",
      "denoise": 1.0,
      "model": ["4", 0],
      "positive": ["6", 0],
      "negative": ["7", 0],
      "latent_image": ["5", 0]
    }
  },
  "4": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": { "ckpt_name": "flux1-dev-fp8.safetensors" }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "a lighthouse at dawn", "clip": ["4", 1] },
    "_meta": { "title": "Positive Prompt" }
  }
}

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.

Vista de cima de uma mesa com notebook, diagrama em papel e café

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.

Técnico verificando cabos em uma sala de servidores estreita

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 mensagemSignificado
statusO tamanho da fila mudou
execution_startSeu prompt saiu da fila e começou a rodar
execution_cachedLista os nós ignorados porque o resultado estava em cache
executingO nó em execução agora; node: null significa que o grafo terminou
progressPasso value de max do amostrador
executedUm nó produziu uma saída, como nomes de arquivos salvos
execution_errorUm nó gerou uma exceção
execution_successO 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"]

Parede de estúdio com impressões de fotos de produto presas com alfinetes

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)

Retocador repintando parte de um retrato em uma mesa digitalizadora

É 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.

Desenvolvedor trabalhando sozinho à noite sob a luz quente de um abajur

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:

TrabalhoModeloPor que escolher
Texto para imagem do dia a diaFlux Dev12B de parâmetros, 11 proporções até 21:9, modo img2img
Fotos e edições baseadas em referênciaFlux 2 ProAté 8 imagens de referência, saídas de até 4 MP
Rascunhos rápidosFlux SchnellPrévias rápidas antes de uma renderização final
Inpainting e remoção de objetosFlux Fill ProRepinta somente a área que você mascarar
Controle de bordas e profundidadeFlux Canny Pro e Flux Depth ProMantém o layout de uma imagem de origem
Outra família de modelosStable Diffusion 3.5 LargeVisual diferente, mesmo fluxo de trabalho

Gere em seis passos

Designer segurando uma foto impressa de um lago de montanha ao lado de um monitor

Aqui está o processo completo com o Flux 2 Pro, o modelo que mais se aproxima de um fluxo de trabalho do ComfyUI com imagem de referência:

  1. Abra a página do Flux 2 Pro no Picasso IA.
  2. Escreva seu prompt. Nomeie o assunto, a luz e a lente, do mesmo jeito que faria em um nó de texto do ComfyUI.
  3. 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.
  4. Escolha uma resolução. O padrão é 1 MP, e o modelo aceita até 4 MP, embora 2 MP ou menos seja o recomendado.
  5. Adicione até 8 imagens de entrada se quiser que o resultado siga um estilo, um rosto ou uma foto de produto.
  6. Defina o formato de saída (WebP, JPG ou PNG) e clique em gerar. Reutilize o seed depois para recriar o mesmo resultado.
ConfiguraçãoPadrãoConselho prático
Resolução1 MPFique em 2 MP ou menos para os melhores resultados
Qualidade de saída80Faixa de 0 a 100, ignorada para PNG
Tolerância de segurança21 é o mais rígido, 5 é o mais permissivo
SeedAleatórioFixe 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.

Compartilhe este artigo

Escolha seu idioma