API
API reference
Every route the proxy serves under /api/v2, with request fields, response shapes, status codes, and error codes. Also covers the error envelope, Idempotency-Key, and the fixed bounds.
Routes
The proxy serves the Comfy API v2 under /api/v2. These are all the routes it registers. A GET route also answers HEAD.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v2/health |
Process probe. Never calls ComfyUI and never needs a token. |
POST |
/api/v2/jobs |
Submit an API-format workflow. |
GET |
/api/v2/jobs |
List jobs this proxy submitted, newest first. |
GET |
/api/v2/jobs/{id} |
The job's current state, read from ComfyUI on every call. |
GET |
/api/v2/jobs/{id}/workflow |
The resolved graph the job ran with. |
POST |
/api/v2/jobs/{id}/cancel |
Request cancellation. |
GET |
/api/v2/jobs/{id}/events |
Live Server-Sent Events stream. See Event stream. |
POST |
/api/v2/assets |
Multipart upload. See Assets. |
POST |
/api/v2/assets/from-hash |
Look up an asset by content hash. |
POST |
/api/v2/assets/from-path |
Register a file already under --comfyui-base-dir. |
HEAD |
/api/v2/assets/by-hash/{hash} |
Check whether a hash is known. |
GET |
/api/v2/assets/{id} |
Asset metadata. |
GET |
/api/v2/assets/{id}/content |
Asset bytes, with Range support. |
DELETE |
/api/v2/assets/{id} |
Remove an asset from the index. |
There are no batch endpoints. One proxy fronts one ComfyUI, and a batch is a series of submits from the client. GET /api/v2/jobs, GET /api/v2/jobs/{id}/workflow, POST /api/v2/assets/from-path, DELETE /api/v2/assets/{id}, and GET /api/v2/health are proxy-local extensions that the vendored Comfy Cloud contract does not list.
Assets, uploads, and downloads are covered on Assets. This page covers jobs, health, and the conventions every route shares.
Authentication and origin checks
When a token is configured, every /api/v2/* request except GET /api/v2/health and OPTIONS preflights must carry it:
Authorization: Bearer <token>
A missing, malformed, or wrong token is 401 unauthorized. The token is compared in constant time.
An origin guard runs in front of auth on every request. A request with Sec-Fetch-Site: cross-site is 403 forbidden_origin, and so is a request whose Origin host does not match a loopback Host. Requests with no Origin header (curl, SDKs) pass. Origins allowlisted with cors_origins skip the guard and receive CORS headers. See Remote access and Configuration.
Error envelope
Every error the proxy writes has the same shape. details is null unless the error carries structured data.
{
"error": {
"code": "missing_asset",
"message": "Unresolvable core/ASSET reference(s): nope",
"details": {"missing_ids": ["nope"]}
}
}
Match on code, not on message. The codes are the reference's exact strings.
| Code | Status | Raised by |
|---|---|---|
invalid_request |
400 or 422 | Malformed body, bad query parameter, bad field value. |
invalid_workflow |
422 | Missing workflow, reserved fields present, or ComfyUI rejected the graph. |
workflow_format_ui |
422 | The workflow is a UI export, not API format. |
missing_asset |
422 | A core/ASSET reference did not resolve. |
idempotency_key_reuse |
422 | The Idempotency-Key was already used. |
not_found |
404 | Unknown job or asset id, or no stored workflow. |
blob_not_found |
404 | from-hash or from-path found nothing. |
output_unavailable |
404 | The asset is known but its bytes are gone. |
hash_mismatch |
409 | expected_hash did not match the upload, or a model file appeared at the destination mid-write. |
payload_too_large |
413 | Upload or registered file over max_upload_mb. |
too_many_streams |
429 | The event stream cap is reached. Sent with Retry-After: 5. |
unauthorized |
401 | Missing or wrong bearer token. |
forbidden_origin |
403 | The origin guard refused the request. |
method_not_allowed |
405 | Wrong method on an assets/by-hash/... or assets/{id}/content path. |
upstream_unreachable |
503 | ComfyUI could not be reached, or answered with invalid JSON. |
upstream_error |
500 or 502 | ComfyUI failed an upload or a content fetch. |
internal_error |
500 | The proxy's own state failed, such as a --state-dir write. The cause is logged, never returned. |
Bounds
These are fixed in the binary and match the reference.
| Bound | Value |
|---|---|
Job and output URL lifetime (expires_at, url_expires_at) |
24 hours from creation or from the read |
Idempotency-Key length |
1 to 255 characters |
Idempotency-Key claims held |
10,000, oldest evicted first |
metadata |
a string of at most 1024 UTF-8 bytes |
priority |
an integer from -1,000,000 to 1,000,000 |
GET /jobs limit |
1 to 100, default 50 |
GET /jobs scan |
at most 500 job records per request |
Job records reloaded from --state-dir at startup |
the newest 500 |
| Concurrent event streams | 8 |
| Upload size | max_upload_mb, default 100 MB |
POST /jobs body |
max_upload_mb plus 1 MiB |
from-hash and from-path body |
1 MiB |
| Multipart parts per upload | 64 |
Health
GET /api/v2/health answers without calling ComfyUI and without a token, so a supervisor can probe it. upstream is the configured ComfyUI URL, not a reachability check.
curl -s http://127.0.0.1:8189/api/v2/health
{"status": "healthy", "upstream": "http://127.0.0.1:8188"}
Submit a job
POST /api/v2/jobs takes a JSON body and returns 201 with the job object.
| Field | Type | Notes |
|---|---|---|
workflow |
object | Required. An API-format graph, as ComfyUI's "Export (API)" writes it. |
extra_data |
object | Optional. The only accepted key is api_key_comfy_org, a string, which authenticates partner (API) nodes. Forwarded to ComfyUI with the prompt and never stored or logged. |
metadata |
string | Optional opaque label, at most 1024 bytes. Stored by the proxy and echoed on reads. Not sent to ComfyUI. |
priority |
integer | Optional and advisory. Stored and echoed, never passed to ComfyUI's queue. |
webhook_url, inputs |
Reserved. Present and non-null is a 422 invalid_workflow. |
curl -s http://127.0.0.1:8189/api/v2/jobs \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 7d1f6c2e-5b0a-4e8f-9a51-3c2b8e7d4f10' \
-d '{"workflow": {...}, "metadata": "demo", "priority": 3}'
{
"id": "813f058f-80fc-4023-82bf-8c20fe3190ca",
"status": "queued",
"created_at": "2026-10-28T01:03:01.505579+00:00",
"started_at": null,
"completed_at": null,
"expires_at": "2026-10-29T01:03:01.505579+00:00",
"queue_position": null,
"progress": null,
"outputs": [],
"error": null,
"outputs_reused": false,
"urls": {
"self": "http://127.0.0.1:8189/api/v2/jobs/813f058f-80fc-4023-82bf-8c20fe3190ca",
"events": "http://127.0.0.1:8189/api/v2/jobs/813f058f-80fc-4023-82bf-8c20fe3190ca/events",
"cancel": "http://127.0.0.1:8189/api/v2/jobs/813f058f-80fc-4023-82bf-8c20fe3190ca/cancel"
},
"metadata": "demo",
"priority": 3
}
The job id is a fresh UUID that the proxy also sends to ComfyUI as prompt_id and client_id.
Before the graph goes to ComfyUI, every core/ASSET reference in it is replaced with the filename ComfyUI's loaders expect. A reference names an uploaded asset id, an output asset id, or a content hash:
{"__type": "core/ASSET", "info": {"id": "31524f5f-055f-4bfc-9d5e-2418342382c2"}}
See Assets for how each kind resolves.
| Status | Code | When |
|---|---|---|
| 201 | Accepted by ComfyUI. | |
| 400 | invalid_request |
Body is not a JSON object, extra_data has another key or a non-string key value, or Idempotency-Key is empty or longer than 255 characters. |
| 422 | invalid_workflow |
workflow missing or not an object, webhook_url or inputs present, or ComfyUI rejected the graph. A rejection carries ComfyUI's node_errors in details. |
| 422 | workflow_format_ui |
The graph has a top-level nodes list and links. |
| 422 | invalid_request |
metadata is not a string or is too long, priority is not an integer or is out of range. |
| 422 | missing_asset |
A core/ASSET reference did not resolve. details.missing_ids lists them. |
| 422 | idempotency_key_reuse |
The key was used before. |
| 503 | upstream_unreachable |
ComfyUI could not be reached. |
When ComfyUI rejects a graph, message is ComfyUI's error.message (or Workflow rejected. when it sent none) and details.node_errors is ComfyUI's node_errors object as sent, or {}:
{
"error": {
"code": "invalid_workflow",
"message": "...",
"details": {"node_errors": {...}}
}
}
The proxy decodes workflow numbers exactly, so a 64-bit seed reaches ComfyUI unchanged. Object keys are re-encoded in sorted order.
Idempotency-Key
Idempotency-Key on POST /api/v2/jobs is single-use. The first submit with a key claims it, and any later submit with the same key is 422 idempotency_key_reuse. There is no replay of the first response: after a reuse error, poll the job or list jobs instead of resubmitting.
The claim is released only when ComfyUI definitely created no job, which is when it answered with a rejection. When ComfyUI could not be reached, the outcome is unknown, so the key stays claimed and a retry with it cannot submit twice.
Claims live in memory, up to 10,000 of them with the oldest evicted first. With --state-dir they are written to SQLite and survive a restart. See State and service.
Get a job
GET /api/v2/jobs/{id} is the authoritative view of a job. Each call reads ComfyUI's /history, then /queue if the job is not in history, and merges the result with the proxy's own record (created_at, metadata, priority).
{
"id": "813f058f-80fc-4023-82bf-8c20fe3190ca",
"status": "succeeded",
"created_at": "2026-10-28T01:03:01.505579+00:00",
"started_at": null,
"completed_at": null,
"expires_at": "2026-10-29T01:03:01.505579+00:00",
"queue_position": null,
"progress": null,
"outputs": [
{
"node_id": "9",
"name": "out.png",
"type": "image",
"content_type": "image/png",
"size_bytes": 1553743,
"id": "eyJqIjogIjgxM2Y...In0.A_8vZszyRfDl...",
"hash": null,
"url": "http://127.0.0.1:8189/api/v2/assets/eyJqIjogIjgxM2Y...In0.A_8vZszyRfDl.../content",
"url_expires_at": "2026-10-29T01:03:01.734220+00:00",
"job_id": "813f058f-80fc-4023-82bf-8c20fe3190ca"
}
],
"error": null,
"outputs_reused": false,
"urls": {"self": "...", "events": "...", "cancel": "..."},
"metadata": "demo",
"priority": 3
}
Job fields
| Field | Notes |
|---|---|
status |
queued, running, succeeded, failed, canceled, or expired. A cancel response can also say canceling. |
created_at, expires_at |
From the proxy's record. expires_at is created_at plus 24 hours. |
started_at, completed_at |
Always null. ComfyUI does not report them. |
queue_position |
1-based position among pending prompts while queued, else null. |
progress |
{"value": 0, "nodes_done": 0, "nodes_total": 0} while running, else null. Live progress is on the event stream. |
outputs |
Every output in the order ComfyUI listed them. Empty until the job is in history. |
error |
null unless failed. Then {"code": "node_execution_error", "message", "node_id", "class_type", "traceback"}, taken from ComfyUI's execution_error. traceback is null. |
outputs_reused |
true when ComfyUI served at least one node from its cache. |
metadata, priority |
Present only when set on submit. Absent, not null, otherwise. |
A job ComfyUI no longer knows but the proxy recorded reads as expired. A job interrupted by a cancel reads as canceled.
Output fields
| Field | Notes |
|---|---|
node_id |
The output node. |
name |
The filename ComfyUI wrote. For a value output, <node>_<key>_<index>.txt. |
type |
image, video, audio, text, file, or latent, from ComfyUI's images, gifs, audio, text, files, latents keys. |
content_type |
Guessed from the file extension. text/plain for a value output. |
size_bytes |
The file's size. ComfyUI's history does not carry sizes, so the proxy asks ComfyUI's /view once per output (a HEAD) and remembers the answer. 0 when ComfyUI cannot say, for example after the file was deleted. |
id |
A signed, stateless output asset id. See Assets. |
hash |
Always null for outputs. |
url, url_expires_at |
The content URL and a time 24 hours after this read. |
The base of every URL in a response is public_url when it is set, else the scheme and Host the client used. X-Forwarded-* headers are ignored, so a client cannot point emitted links at another origin. Behind a TLS-terminating reverse proxy, set public_url instead.
Value outputs
Some nodes return a value instead of writing a file: PreviewAny returns text, for example,
and SaveText returns the text as well as the file it saves. ComfyUI's history holds these as
raw strings under the node's key, and each becomes its own output entry: type from the key
(text for text), content_type text/plain, size_bytes the length of the value in
bytes, name <node>_<key>_<index>.txt, and an id and url like any other output. Its
content is served from ComfyUI's history as text/plain; charset=utf-8 with
X-Content-Type-Options: nosniff, so it is never rendered as HTML or run as script. A
non-string value in that list is skipped. The Python proxy drops value outputs entirely
(differences).
| Status | Code | When |
|---|---|---|
| 200 | Known to ComfyUI or to the proxy. | |
| 404 | not_found |
Unknown to both, or the id is not a UUID. |
| 500 | internal_error |
A --state-dir read failed. |
| 503 | upstream_unreachable |
ComfyUI could not be reached or returned invalid JSON. |
List jobs
GET /api/v2/jobs lists the jobs this proxy submitted, newest first, each resolved against ComfyUI as GET /jobs/{id} would.
| Query | Notes |
|---|---|
limit |
1 to 100. Default 50. |
status |
Comma-separated filter: any of queued, running, succeeded, canceling, canceled, failed, expired. |
curl -s 'http://127.0.0.1:8189/api/v2/jobs?limit=20&status=succeeded,failed'
{"jobs": [{"id": "813f058f-...", "status": "succeeded", ...}], "truncated": false}
There is no cursor. Each request walks the newest records and stops after 500 of them, because each one costs two ComfyUI reads. truncated: true means the walk stopped before it ran out of records, so more matches may exist further back. truncated: false means there are no more matches in memory.
The list covers the in-memory window only. With --state-dir, the newest 500 records reload at startup. Older records still answer GET /jobs/{id} but do not appear in the list.
| Status | Code | When |
|---|---|---|
| 200 | ||
| 400 | invalid_request |
limit is not an integer or is out of range, or status names an unknown value. |
| 503 | upstream_unreachable |
ComfyUI could not be reached. |
Get a job's workflow
GET /api/v2/jobs/{id}/workflow returns the graph the job ran with, after core/ASSET references were resolved. It is the proxy's own stored copy, never ComfyUI's history copy, which carries the partner API key.
{"format": "api", "workflow": {"1": {"class_type": "EmptyImage", "inputs": {...}}}}
| Status | Code | When |
|---|---|---|
| 200 | ||
| 404 | not_found |
The id is not a UUID, or the proxy holds no graph for it (for example, after a restart without --state-dir). |
Cancel a job
POST /api/v2/jobs/{id}/cancel asks ComfyUI to cancel the job and returns the job object, as GET /jobs/{id} would. A running job reads canceling until the interrupt lands at the next node boundary, then canceled. Cancelling a finished job returns its terminal state. Poll the job or watch the event stream for the final status.
A job the proxy does not remember (after a restart without --state-dir) can still be cancelled while ComfyUI knows it.
| Status | Code | When |
|---|---|---|
| 200 | ||
| 404 | not_found |
Unknown to the proxy and to ComfyUI, or the id is not a UUID. |
| 503 | upstream_unreachable |
ComfyUI could not be reached. |