Comfy Cloudでワークフローを組み、色々遊んでいる日々が続いています。
ブラウザで一つずつ実行するのは便利ですが、スクリプトやアプリから同じワークフローを回したいときは API が必要になります。
Cloud API Overview をベースに、Postman と Python で実際に通した流れを残します。
Comfy Cloud の公式サイト
※ 本記事には ComfyUI のアフィリエイトリンク(招待リンク)を含みます。
前提
| 項目 | 内容 |
|---|---|
| プラン | API 実行は有料プラン(Standard / Creator / Pro)が必要。Free 枠では API でワークフローを実行できません(公式ドキュメント) |
| API の性質 | Experimental API。エンドポイントやレスポンス形式が変わる可能性があります |
| クレジット | Web UI と 同じ月間クレジットから消費されます |
| ベース URL | https://cloud.comfy.org |
| 認証 | すべてのリクエストに X-API-Key ヘッダーが必要 |
まだアカウントがない場合は、下記の招待リンクから登録できます。
公式 Quick Start の流れは次の 4 つです。
- ワークフローを API 形式で用意する
POST /api/promptでジョブを投入する- ポーリングまたは WebSocket で完了を待つ
/api/view(またはジョブ詳細 API)で出力を取る
flowchart LR
A[ブラウザでワークフロー作成] --> B[API 形式でエクスポート]
B --> C["POST /api/prompt"]
C --> D[進捗監視]
D --> E["/api/view でダウンロード"]
ワークフローを API 形式で用意する
まずはブラウザの Comfy Cloud で、テンプレートからワークフローを用意します。
今回は 画像カテゴリの Z-Image-Turbo(テキストから画像)をベースに、シンプルな test-api ワークフローを使いました。
テンプレート一覧から画像系を選ぶ画面API 向けテンプレートのワークフロー例
注意するところはこちらです。
- SaveImage ノードまで繋がっていること
- 差し替えたい値(プロンプト、seed など)がどのノードの
inputsか - Partner Nodes を使うなら、ブラウザ上で一度正常に動くこと
Cloud API に渡す JSON は、通常の「保存形式」とは別物です。
Workflow API Format によると、API 形式で書き出す必要があります。
| 観点 | 保存形式(Save) | API 形式(Export Workflow API) |
|---|---|---|
| メニュー | File → Save | File → Export Workflow (API) |
| ノードのキー | タイトルやラベル | 数値のノード ID |
| UI 情報 | 位置・色・グループなど含む | 除外される |
| 用途 | エディタで再編集 | API 投入用 |
エディタで ファイル → エクスポート (API)(英語 UI なら File → Export Workflow (API))を選び、test-api.json として保存しました。
ファイルメニューからエクスポート (API) を選ぶ画面
今回の test-api の中身は次のとおりです。
{
"9": {
"inputs": {
"filename_prefix": "test_api",
"images": ["57:8", 0]
},
"class_type": "SaveImage",
"_meta": {
"title": "画像を保存"
}
},
"62": {
"inputs": {
"filename_prefix": "ComfyUI",
"format": "png",
"format.bit_depth": "8-bit",
"format.input_color_space": "sRGB"
},
"class_type": "SaveImageAdvanced",
"_meta": {
"title": "Save Image (Advanced)"
}
},
"57:30": {
"inputs": {
"clip_name": "qwen_3_4b.safetensors",
"type": "lumina2",
"device": "default"
},
"class_type": "CLIPLoader",
"_meta": {
"title": "CLIPを読み込む"
}
},
"57:29": {
"inputs": {
"vae_name": "ae.safetensors"
},
"class_type": "VAELoader",
"_meta": {
"title": "VAEを読み込む"
}
},
"57:33": {
"inputs": {
"conditioning": ["57:27", 0]
},
"class_type": "ConditioningZeroOut",
"_meta": {
"title": "条件付けゼロアウト"
}
},
"57:8": {
"inputs": {
"samples": ["57:3", 0],
"vae": ["57:29", 0]
},
"class_type": "VAEDecode",
"_meta": {
"title": "VAEデコード"
}
},
"57:28": {
"inputs": {
"unet_name": "z_image_turbo_bf16.safetensors",
"weight_dtype": "default"
},
"class_type": "UNETLoader",
"_meta": {
"title": "拡散モデルを読み込む"
}
},
"57:27": {
"inputs": {
"text": "input text",
"clip": ["57:30", 0]
},
"class_type": "CLIPTextEncode",
"_meta": {
"title": "CLIPテキストエンコード(プロンプト)"
}
},
"57:13": {
"inputs": {
"width": 1024,
"height": 1024,
"batch_size": 1
},
"class_type": "EmptySD3LatentImage",
"_meta": {
"title": "空のSD3潜在画像"
}
},
"57:11": {
"inputs": {
"shift": 3,
"model": ["57:28", 0]
},
"class_type": "ModelSamplingAuraFlow",
"_meta": {
"title": "モデルサンプリングオーラフロー"
}
},
"57:3": {
"inputs": {
"seed": 0,
"steps": 8,
"cfg": 1,
"sampler_name": "res_multistep",
"scheduler": "simple",
"denoise": 1,
"model": ["57:11", 0],
"positive": ["57:27", 0],
"negative": ["57:33", 0],
"latent_image": ["57:13", 0]
},
"class_type": "KSampler",
"_meta": {
"title": "Kサンプラー"
}
}
}実際にプログラマティックに入力などをいじるならノードはだいたいここです。
| やりたいこと | ノード ID | フィールド |
|---|---|---|
| プロンプト変更 | "57:27" | inputs.text |
| seed 変更 | "57:3" | inputs.seed |
| 出力プレフィックス | "9" | inputs.filename_prefix |
API キーを取ってジョブを送る
認証には Comfy Platform の API キーを使います。
手順は Getting an API Key にあります。
- platform.comfy.org/login にログイン
- API Keys で
+ 新しいAPIキー - 名前を入れて
生成 - 表示されたキーをすぐに保存(再表示不可)
ComfyUI APIキー一覧と「新しいAPIキー」ボタン新しいAPIキーを生成するダイアログ生成直後のキー表示(一度だけ表示される)
export COMFY_CLOUD_API_KEY="comfyui-xxxxxxxxxxxx"
export BASE_URL="https://cloud.comfy.org"最初の確認は GET /api/user です。僕は Postman から始めました。
curl -X GET "$BASE_URL/api/user" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"200 OK で {"id":"...","status":"active"} が返れば、キーは生きています。
以降の Postman 操作も、Authorization は同じ API Key → Header の X-API-Key です。
Postman で GET /api/user が 200 OK になった画面
POST /api/prompt で投入する
公式 Quick Start どおり、POST /api/prompt に API 形式の JSON を prompt フィールドで渡します。
つまずきやすいのは、test-api.json を Body のルートにそのまま貼ってしまうこと。
必ず {"prompt": { ...test-api.json の中身... }} にします。
curl -X POST "$BASE_URL/api/prompt" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": '"$(cat test-api.json)"'}'成功すると、だいたい次の形で prompt_id が返ります。これが以降のジョブ ID です。
{
"prompt_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"number": 1,
"node_errors": {}
}Postman で POST /api/prompt が成功し prompt_id が返った画面
実運用では、JSON を読み込んで seed やプロンプトを差し替えてから送るパターンが多いです。
import os
import json
import requests
BASE_URL = "https://cloud.comfy.org"
API_KEY = os.environ["COMFY_CLOUD_API_KEY"]
def get_headers():
return {"X-API-Key": API_KEY, "Content-Type": "application/json"}
with open("test-api.json") as f:
workflow = json.load(f)
# ノード ID は test-api.json に合わせる(ワークフローごとに違う)
workflow["57:3"]["inputs"]["seed"] = 42
workflow["57:27"]["inputs"]["text"] = "a cat wearing sunglasses, studio photo"
response = requests.post(
f"{BASE_URL}/api/prompt",
headers=get_headers(),
json={"prompt": workflow},
)
response.raise_for_status()
prompt_id = response.json()["prompt_id"]
print(f"Job submitted: {prompt_id}")進捗を待つ(実測の status)
ジョブは非同期です。
手軽なのは GET /api/job/{prompt_id}/status のポーリングです。
| status | 意味 |
|---|---|
waiting_to_dispatch | ディスパッチ待ち |
pending | キュー待ち |
in_progress | 実行中 |
success / completed | 成功(実APIは success、Quick Start 例は completed) |
error / failed | 失敗 |
cancelled | キャンセル |
僕が Postman で叩いた成功時のレスポンスは、公式 OpenAPI の JobStatusResponse どおり次の形でした。
{
"id": "1db59ce7-330d-4218-b889-ce92cbd63e5d",
"status": "success",
"created_at": "2026-07-15T10:10:35.257831Z",
"updated_at": "2026-07-15T10:10:41.014223Z",
"last_state_update": "2026-07-15T10:10:41.014223Z",
"assigned_inference": "10.4.35.14:8080",
"error_message": null
}Postman で GET /api/job/…/status が success になった画面
ここで大事なのは、status API にはファイル名が入らないことです。
完了判定には足りますが、ダウンロード用の filename は次のジョブ詳細から取ります。
import time
def poll_for_completion(prompt_id: str, timeout: int = 300, poll_interval: float = 2.0) -> None:
start_time = time.time()
while time.time() - start_time < timeout:
status_res = requests.get(
f"{BASE_URL}/api/job/{prompt_id}/status",
headers=get_headers(),
)
status_res.raise_for_status()
status = status_res.json()["status"]
if status in ("success", "completed"):
return
if status in ("error", "failed", "cancelled"):
raise RuntimeError(f"Job failed with status: {status}")
time.sleep(poll_interval)
raise TimeoutError(f"Job {prompt_id} did not complete within {timeout}s")
poll_for_completion(prompt_id)進捗バーやノード単位のログが欲しいときは WebSocket です。
wss://cloud.comfy.org/ws?clientId={uuid}&token={api_key}Postman で接続したとき、最初に返ってきたのは次でした。
{
"type": "status",
"data": {
"status": {
"exec_info": {
"queue_remaining": 0
}
},
"sid": "31d0726d-635d-4a94-aaf5-1c862db38bbc"
}
}キュー状況はリアルタイムで見えます。
ただ、画像の filename まではここからは見えませんでした。
成果物を取るなら、次のジョブ詳細 API の方がわかりやすかったです。
出力を取る(実測の outputs)
ダウンロードには filename(と必要なら subfolder / type) が必要です。
取得先は GET /api/jobs/{prompt_id}(Get full job details)。
さっきの /api/job/.../status とはパスが違います(job 単数 vs jobs 複数)。
curl -X GET "$BASE_URL/api/jobs/$PROMPT_ID" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY"僕が実際に叩いたレスポンスは、ざっくり次の形でした(長い workflow や execution_status.messages は省略)。
{
"id": "1db59ce7-330d-4218-b889-ce92cbd63e5d",
"status": "completed",
"outputs_count": 1,
"execution_status": {
"completed": true,
"status_str": "success"
},
"outputs": {
"9": {
"images": [
{
"display_name": "test_api_00001_.png",
"filename": "7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png",
"subfolder": "",
"type": "output"
}
]
}
},
"preview_output": {
"filename": "7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png",
"mediaType": "images",
"nodeId": "9",
"subfolder": "",
"type": "output"
}
}Postman でジョブ詳細の outputs(filename / display_name)を確認した画面
| フィールド | 中身の例 | 用途 |
|---|---|---|
filename | ハッシュっぽい長い .png | /api/view に渡す実体名 |
display_name | test_api_00001_.png | 人向けの表示名(SaveImage の filename_prefix 由来) |
subfolder | "" | 指定していなければ空 |
type | output | /api/view にも渡す |
/api/view には必ず filename(ハッシュ側) を使います。display_name を渡すと取れません。
curl -L "$BASE_URL/api/view?filename=7e4a58ad6987c5ab0e833e1933ae88409f2d16cafcb46258dd917b55b321a5f5.png&subfolder=&type=output" \
-H "X-API-Key: $COMFY_CLOUD_API_KEY" \
-o test_api_00001_.pngPostman で /api/view から画像プレビューを取得した画面
Python なら、ジョブ詳細の outputs を回して保存するだけです。
job_res = requests.get(
f"{BASE_URL}/api/jobs/{prompt_id}",
headers=get_headers(),
)
job_res.raise_for_status()
outputs = job_res.json()["outputs"]
for node_outputs in outputs.values():
for file_info in node_outputs.get("images", []):
params = {
"filename": file_info["filename"],
"subfolder": file_info.get("subfolder", ""),
"type": file_info.get("type", "output"),
}
view_res = requests.get(
f"{BASE_URL}/api/view",
headers=get_headers(),
params=params,
)
view_res.raise_for_status()
save_as = file_info.get("display_name") or file_info["filename"]
with open(save_as, "wb") as f:
f.write(view_res.content)
print(f"Downloaded: {save_as}")API 経由で回したあと、ブラウザ側のワークフロー画面でも同じ結果が確認できます。
SaveImage のプレフィックス test_api から、サイドバーに test_api_00001_ として並びました。
API 実行後、ブラウザの SaveImage に結果が表示された画面
ブラウザ UI で Run したときと同じ品質の PNG が手元に落ちれば、一連の流れは通っています。
1本にした Python サンプル
上の流れを 1 本にすると、だいたい次の形になります(公式 Complete Example をベースに整理)。
import os
import json
import time
import requests
BASE_URL = "https://cloud.comfy.org"
API_KEY = os.environ["COMFY_CLOUD_API_KEY"]
def get_headers():
return {"X-API-Key": API_KEY, "Content-Type": "application/json"}
def main():
with open("test-api.json") as f:
workflow = json.load(f)
workflow["57:3"]["inputs"]["seed"] = 42
workflow["57:27"]["inputs"]["text"] = "a beautiful sunset"
response = requests.post(
f"{BASE_URL}/api/prompt",
headers=get_headers(),
json={"prompt": workflow},
)
response.raise_for_status()
prompt_id = response.json()["prompt_id"]
print(f"Job submitted: {prompt_id}")
while True:
status_res = requests.get(
f"{BASE_URL}/api/job/{prompt_id}/status",
headers=get_headers(),
)
status_res.raise_for_status()
status = status_res.json()["status"]
if status in ("success", "completed"):
break
if status in ("error", "failed", "cancelled"):
raise RuntimeError(f"Job {status}")
time.sleep(2)
job_res = requests.get(
f"{BASE_URL}/api/jobs/{prompt_id}",
headers=get_headers(),
)
job_res.raise_for_status()
outputs = job_res.json()["outputs"]
for node_outputs in outputs.values():
for file_info in node_outputs.get("images", []):
params = {
"filename": file_info["filename"],
"subfolder": file_info.get("subfolder", ""),
"type": "output",
}
view_res = requests.get(
f"{BASE_URL}/api/view",
headers=get_headers(),
params=params,
)
view_res.raise_for_status()
save_as = file_info.get("display_name") or file_info["filename"]
with open(save_as, "wb") as f:
f.write(view_res.content)
print(f"Downloaded: {save_as}")
if __name__ == "__main__":
main()ローカル ComfyUI 向けの自動化がある場合も、ベース URL と API キーを差し替えるだけでかなり流用しやすいのが、この API の強みでした。
試してみて感じたこと
- ブラウザで動いたワークフローは、Export Workflow API でそのまま API 化しやすい
- 入力差し替えは JSON の
inputsを書き換えるだけなので、バッチ生成と相性がいい - 最初のAPIを使っての確認は Postman で
/api/user→/api/promptで試すのが良さげ - ジョブ詳細の
outputsではfilenameがハッシュ、display_nameが読みやすい名前。/api/viewにはハッシュ側を渡す - Free 枠では API 実行不可なので、検証時点で有料プランが必要
- Experimental 表記どおり、公式ドキュメントはたまに見直した方がよさそう
並列実行の上限はプラン依存です(Standard 1 / Creator 3 / Pro 5 ジョブ)。
複数プロンプトを一気に投げる場合は 公式の並列サンプル も参考になります。
Comfy Cloud の招待リンク
API を本格的に使う場合は、Standard 以上のプランと platform.comfy.org での API キー発行が必要です。
参考リンク
- Comfy Cloud 公式(アフィリエイトリンク)
- Cloud API Overview — 公式 Quick Start
- Cloud API Reference — エンドポイント詳細
- Workflow API Format — API 形式の説明
- Getting an API Key — API キー取得
- Comfy Cloud 料金
まとめ
公式ドキュメントは揃っていますが、写真が少ないので、初回の疎通記録として残しました。
実測で効いたのは、status と jobs の役割分担と、filename(ハッシュ)と display_name の使い分けです。
自動化や量産に進む前に、まずは 1 枚画像を API で出すところまで通してみるのが近道でした。