curl --request POST \
--url http://127.0.0.1:8189/api/v2/jobs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"workflow": {},
"extra_data": {
"api_key_comfy_org": "<string>",
"auth_token_comfy_org": "<string>"
}
}
'import requests
url = "http://127.0.0.1:8189/api/v2/jobs"
payload = {
"workflow": {},
"extra_data": {
"api_key_comfy_org": "<string>",
"auth_token_comfy_org": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
workflow: {},
extra_data: {api_key_comfy_org: '<string>', auth_token_comfy_org: '<string>'}
})
};
fetch('http://127.0.0.1:8189/api/v2/jobs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "8189",
CURLOPT_URL => "http://127.0.0.1:8189/api/v2/jobs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'workflow' => [
],
'extra_data' => [
'api_key_comfy_org' => '<string>',
'auth_token_comfy_org' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://127.0.0.1:8189/api/v2/jobs"
payload := strings.NewReader("{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://127.0.0.1:8189/api/v2/jobs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:8189/api/v2/jobs")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b",
"status": "queued",
"created_at": "2023-11-07T05:31:56Z",
"started_at": "2023-11-07T05:31:56Z",
"completed_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"queue_position": 123,
"progress": {
"value": 0.42,
"nodes_done": 11,
"nodes_total": 31,
"current_node": "12",
"current_node_class": "KSampler",
"step": 21,
"steps": 50,
"message": "KSampler 21/50"
},
"outputs": [
{
"node_id": "9",
"name": "ComfyUI_00001_.png",
"type": "image",
"content_type": "image/png",
"size_bytes": 1848320,
"id": "9f8a1c0d-2b3e-4f56-...",
"hash": "<string>",
"url": "<string>",
"url_expires_at": "2023-11-07T05:31:56Z",
"job_id": "<string>"
}
],
"error": {
"code": "node_execution_error",
"message": "<string>",
"node_id": "<string>",
"class_type": "<string>",
"traceback": "<string>"
},
"urls": {
"self": "<string>",
"events": "<string>",
"cancel": "<string>",
"logs": "<string>"
},
"metrics": {
"queue_ms": 9000,
"execution_ms": 42000
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}Submit a workflow for execution
Accepts the API-format workflow graph verbatim. Validation is
synchronous: graph structure, unknown node classes, and asset
references (core/ASSET objects — every referenced id must exist
and be owned by the caller). A 201 means the job is durably
recorded and queued.
UI-format workflow JSON (the export with nodes/links) is
rejected with workflow_format_ui.
Idempotency-Key is single-use (reject-on-duplicate, NOT
record-and-replay): the first request to present a given key is
processed normally; ANY later request presenting the same key — a
retry, a concurrent duplicate, or a same-key request with a different
body — is rejected 422 idempotency_key_reuse and is never
re-executed. The key is claimed only for a request that actually
reaches submission and is released if that submission definitively
fails without creating a job (a validation error, or an upstream
reject such as out-of-credits or queue-full), so a legitimate retry
with the same key can proceed. If a submission’s outcome is unknown
(an upstream timeout or 5xx where the job may or may not have been
created), the key stays claimed and the retry is rejected: poll or
list your jobs to find the possibly-created job rather than
resubmitting. Keys expire after 24h. There is no response replay and
no Idempotency-Replayed header.
Reserved for post-MVP and rejected if present today: webhook_url,
inputs.
curl --request POST \
--url http://127.0.0.1:8189/api/v2/jobs \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"workflow": {},
"extra_data": {
"api_key_comfy_org": "<string>",
"auth_token_comfy_org": "<string>"
}
}
'import requests
url = "http://127.0.0.1:8189/api/v2/jobs"
payload = {
"workflow": {},
"extra_data": {
"api_key_comfy_org": "<string>",
"auth_token_comfy_org": "<string>"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
workflow: {},
extra_data: {api_key_comfy_org: '<string>', auth_token_comfy_org: '<string>'}
})
};
fetch('http://127.0.0.1:8189/api/v2/jobs', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_PORT => "8189",
CURLOPT_URL => "http://127.0.0.1:8189/api/v2/jobs",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'workflow' => [
],
'extra_data' => [
'api_key_comfy_org' => '<string>',
'auth_token_comfy_org' => '<string>'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "http://127.0.0.1:8189/api/v2/jobs"
payload := strings.NewReader("{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("http://127.0.0.1:8189/api/v2/jobs")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("http://127.0.0.1:8189/api/v2/jobs")
http = Net::HTTP.new(url.host, url.port)
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"workflow\": {},\n \"extra_data\": {\n \"api_key_comfy_org\": \"<string>\",\n \"auth_token_comfy_org\": \"<string>\"\n }\n}"
response = http.request(request)
puts response.read_body{
"id": "7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b",
"status": "queued",
"created_at": "2023-11-07T05:31:56Z",
"started_at": "2023-11-07T05:31:56Z",
"completed_at": "2023-11-07T05:31:56Z",
"expires_at": "2023-11-07T05:31:56Z",
"queue_position": 123,
"progress": {
"value": 0.42,
"nodes_done": 11,
"nodes_total": 31,
"current_node": "12",
"current_node_class": "KSampler",
"step": 21,
"steps": 50,
"message": "KSampler 21/50"
},
"outputs": [
{
"node_id": "9",
"name": "ComfyUI_00001_.png",
"type": "image",
"content_type": "image/png",
"size_bytes": 1848320,
"id": "9f8a1c0d-2b3e-4f56-...",
"hash": "<string>",
"url": "<string>",
"url_expires_at": "2023-11-07T05:31:56Z",
"job_id": "<string>"
}
],
"error": {
"code": "node_execution_error",
"message": "<string>",
"node_id": "<string>",
"class_type": "<string>",
"traceback": "<string>"
},
"urls": {
"self": "<string>",
"events": "<string>",
"cancel": "<string>",
"logs": "<string>"
},
"metrics": {
"queue_ms": 9000,
"execution_ms": 42000
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}{
"error": {
"code": "invalid_workflow",
"message": "Node 12 (KSampler): required input 'model' is not connected",
"details": {
"node_errors": {
"12": [
{
"field": "model",
"reason": "missing_input"
}
]
}
}
}
}授权
Authorization: Bearer <api-key> — account-scoped API keys on Cloud and serverless. Self-hosted accepts unauthenticated requests by default and can be configured with a static bearer token.
请求头
Client-generated UUID (recommended). Single-use: the first request to present a key is processed; any later request with the same key is rejected 422 idempotency_key_reuse (reject-on-duplicate, no response replay). Keys expire after 24h.
请求体
API-format workflow graph, verbatim.
Per-prompt ComfyUI extra_data, same shape as Comfy Cloud and local ComfyUI. Closed object: only the enumerated keys are accepted, keeping the contract fully typed. Forwarded to the worker per-prompt and excluded from idempotency comparison. On a deployment it is dispatch-only and never stored; on Comfy Cloud it is persisted with the prompt, because the worker needs it, and redacted on every path that returns a workflow to a caller.
Send the one credential you hold: an API key as api_key_comfy_org, or the session token an interactively signed-in client has instead as auth_token_comfy_org. Sending both is accepted and both are forwarded, but it is not a supported combination and which one a node uses is not defined here. Note a session token is short-lived and is not re-minted for you, so one submitted long before it executes may expire in the queue.
Show child attributes
Show child attributes
响应
Job created and queued.
One execution of a workflow. Durable from creation until expires_at; outputs populates incrementally during execution.
"7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b"
Lifecycle: queued → running → succeeded | failed | expired; a cancel request, or the deletion of the deployment the job is running on, moves running → canceling → canceled. Terminal states: succeeded, canceled, failed, expired.
queued, running, succeeded, canceling, canceled, failed, expired Retention deadline — a platform property, not an API constant.
The latest progress snapshot; same data the SSE stream pushes.
Show child attributes
Show child attributes
Show child attributes
Show child attributes
Execution failure detail, carried in job.error (not an HTTP error).
Show child attributes
Show child attributes
Embedded follow-up links — follow these, don't build URLs. A link is either an absolute URL or a host-relative reference (leading /) that already includes any prefix the serving surface is mounted under (e.g. a serverless gateway's /deployment/{deployment_id}/api/v2). Clients MUST resolve a host-relative link against the request origin (scheme + authority), never against a configured base URL — joining it to a base URL that carries the same mount prefix duplicates the prefix.
Show child attributes
Show child attributes
Values are nullable (a metric not yet available — e.g. execution_ms before a job starts running — is null, not omitted); the example below is deliberately all-non-null purely to work around a Spectral/nimma lint-tooling crash on a literal null inside a schema example combined with additionalProperties.nullable: true — the schema itself is unchanged and still allows null values at runtime.
Show child attributes
Show child attributes
{ "queue_ms": 9000, "execution_ms": 42000 }