Independent project. Not affiliated with or endorsed by Comfy Org.

All pages

API

Assets

Uploading inputs and model files, content-hash dedup, downloads with Range, signed output asset ids, and the guards on model placement under --comfyui-base-dir.

View as Markdown Source on GitHub

Overview

An asset is a file the proxy can hand to a workflow or serve back to a client. There are three kinds:

Kind Id Where the bytes live
Uploaded input A UUID (or ComfyUI's own asset id, when its asset catalog returns one) ComfyUI's input/ directory, via ComfyUI's /upload/image
Placed or registered file A UUID On disk under --comfyui-base-dir
Job output A signed output id ComfyUI's output directory, read through /view

Uploaded and registered assets are kept in an index in memory, written through to SQLite with --state-dir. Output ids are not stored anywhere: the id carries its own signed reference.

The proxy fronts a single-user ComfyUI, so there is one namespace. Every client sees every asset.

Upload

POST /api/v2/assets takes multipart/form-data.

Part Notes
file Required. The bytes. Its part Content-Type is the asset's content type, application/octet-stream when absent.
file_path Required. Where the file goes. See Destination paths.
content_type Optional. Overrides the file part's content type.
expected_hash Optional. blake3:<hex>. Compared case-insensitively to the received bytes. A mismatch is 409 hash_mismatch.
expires_in Optional. Seconds until the asset record expires. 0 means never. A value that is not an integer is ignored.
tags Optional, repeatable. Stored with the record.
curl -s http://127.0.0.1:8189/api/v2/assets \
  -F 'file=@photo.png;type=image/png' \
  -F file_path=photo.png \
  -F expires_in=3600
{
  "id": "31524f5f-055f-4bfc-9d5e-2418342382c2",
  "hash": "blake3:a38747620a3c8e134e2c836e994b2ac307601d7523b86f686bf1a27be09b9566",
  "size_bytes": 17,
  "content_type": "image/png",
  "file_path": "photo.png",
  "created_at": "2026-10-28T01:03:17.075514+00:00",
  "url": "http://127.0.0.1:8189/api/v2/assets/31524f5f-055f-4bfc-9d5e-2418342382c2/content",
  "url_expires_at": "2026-10-29T01:03:17.075515+00:00",
  "created_new": true,
  "expires_at": "2026-10-28T02:03:17.000000+00:00"
}

expires_at is present only when expires_in set one. created_new is true when this upload created the record and false when the bytes were already known.

Streaming and the size cap

The file part is written to a temp file while it is hashed, then streamed onward from that file, to ComfyUI or into the model directory. The upload is never held in memory whole. Reading stops one byte past max_upload_mb (default 100 MB), which answers 413 payload_too_large. The temp file is removed when the request ends.

Input limits

Rule Result
More than 64 parts 422 invalid_request
file_path or content_type contains a control character (below U+0020, or U+007F) 422 invalid_request
expires_in lands after 9999-12-31T23:59:59Z, or before the Unix epoch 422 invalid_request

The control-character rule keeps a value from injecting headers into the multipart request sent to ComfyUI.

Status codes

Status Code When
201 A new asset.
200 The bytes match an existing asset, which is returned with created_new: false.
409 hash_mismatch expected_hash differs from the received bytes, or a model file appeared at the destination during the write.
413 payload_too_large The file is larger than max_upload_mb.
422 invalid_request Not multipart, malformed body, missing file or file_path, a rejected path, a failed model check, or a limit above.
500 upstream_error ComfyUI could not be reached for an input upload, or rejected it.

The order matters for dedup: file_path is validated first, then expected_hash, then the hash lookup. Bytes the proxy already holds return the existing asset before any model placement check runs.

Destination paths

file_path decides where an upload goes.

file_path Destination
photo.png A workflow input, uploaded to ComfyUI's input/
input/refs/photo.png A workflow input in the refs subfolder of input/
checkpoints/model.safetensors A model file, placed under <base dir>/models/checkpoints/
models/loras/style.safetensors The same as loras/style.safetensors. A leading models/ is dropped.

Every path is checked before anything is written:

  • It must be relative. A leading / or \ is refused.
  • No segment may be .., anywhere in the path. This runs before the path is classified, so input/../checkpoints/x.safetensors cannot pass as an input and skip the model checks.
  • A path with a directory must start with an allowlisted root: input, or one of the model roots below.

ComfyUI may rename an input upload, for example when the name is taken. The record keeps the name ComfyUI returned, and that name is what core/ASSET references resolve to.

Model placement

ComfyUI's own upload endpoint writes only to input, output, and temp. The proxy can place model weights itself, when it shares a filesystem with ComfyUI and was started with --comfyui-base-dir pointing at the ComfyUI install root. Without it, a model upload is 422 invalid_request and input uploads work as usual.

The allowlisted model roots are the weight directories directly under ComfyUI's models/:

audio_encoders  background_removal  checkpoints  classifiers  clip_vision
controlnet  diffusers  diffusion_models  embeddings  frame_interpolation
geometry_estimation  gligen  hypernetworks  latent_upscale_models  loras
model_patches  optical_flow  photomaker  style_models  text_encoders
upscale_models  vae  vae_approx

custom_nodes and configs are left out on purpose. One holds arbitrary Python and the other YAML that loaders read.

A model upload must pass each of these before it lands:

Guard Refusal
The name ends in .safetensors. 422
The file parses as safetensors: an 8-byte little-endian header length (non-zero, at most 100 MiB, and within the file), then a UTF-8 JSON object whose entries, apart from __metadata__, each carry dtype, shape, and data_offsets. The tensor bytes are never read, and no pickle path exists. 422
The destination, with every existing symlink along it resolved, is still under the resolved base directory. 422
Nothing exists at the destination. 422

Then the bytes go to a hidden temp file in the destination directory, so the final rename stays on one filesystem. The temp file is set to mode 0644, so a ComfyUI running as another user can read it. The destination is claimed with an O_CREAT|O_EXCL create, which fails if another upload got there first (409 hash_mismatch), and the temp file is renamed over the empty claim. A reader never sees a partial file, and an existing file is never overwritten.

The record's file_path is relative to the model root, without the category: model.safetensors, not checkpoints/model.safetensors. That is how ComfyUI's loader widgets name a model, so a core/ASSET reference to the asset resolves to a value the loader accepts.

Register a host file

POST /api/v2/assets/from-path registers a file already under --comfyui-base-dir without copying it.

Field Notes
path Required. A host path. Symlinks are resolved before the checks.
file_path Optional display path that replaces the derived one.
content_type Optional. Guessed from the extension when absent.
curl -s http://127.0.0.1:8189/api/v2/assets/from-path \
  -H 'Content-Type: application/json' \
  -d '{"path": "/opt/ComfyUI/models/loras/style.safetensors"}'

The resolved file must be a regular file under the base directory, inside input/, output/, temp/, or models/. A file under models/ passes the same root allowlist and safetensors header check as an upload. The file is hashed with blake3, and a hash the proxy already holds returns the existing asset.

Status Code When
201 Registered.
200 The bytes match an existing asset.
400 invalid_request The body is not JSON.
404 blob_not_found The path does not exist or is not a regular file.
413 payload_too_large The file is larger than max_upload_mb.
422 invalid_request No --comfyui-base-dir, missing path, a path outside the base directory or the four roots, or a failed model check.

Dedup by hash

Every asset hash is blake3: followed by 64 hex digits, the 32-byte BLAKE3 digest of the bytes. The first record with a given hash owns it, and later identical bytes resolve to that record. A client can check before uploading:

curl -sI http://127.0.0.1:8189/api/v2/assets/by-hash/blake3:a38747620a3c8e134e2c836e994b2ac307601d7523b86f686bf1a27be09b9566

HEAD /api/v2/assets/by-hash/{hash} answers 200 or 404 with no body.

POST /api/v2/assets/from-hash with {"hash": "blake3:..."} returns the existing asset with created_new: false. A miss is 404 blob_not_found. A missing hash is 422 invalid_request, and a body that is not JSON is 400 invalid_request.

Metadata and downloads

GET /api/v2/assets/{id} returns the asset record for an uploaded or registered id. For an output id, it returns a record built from the id, with hash: null, size_bytes from ComfyUI's /view (the same number the job reports, 0 when ComfyUI cannot say), file_path set to the output filename, and job_id. An unknown or invalid id is 404 not_found.

GET /api/v2/assets/{id}/content returns the bytes, and HEAD returns the headers alone. Both accept Range.

curl -s -i -H 'Range: bytes=0-4' \
  http://127.0.0.1:8189/api/v2/assets/31524f5f-055f-4bfc-9d5e-2418342382c2/content
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 5
Content-Range: bytes 0-4/69
Content-Type: image/png

A placed or registered file is served from disk, with Range handled by the proxy. An input or output held by ComfyUI is streamed from ComfyUI's /view, with Range passed through and Content-Length, Content-Range, and Accept-Ranges copied back. The body is copied in chunks, so a large output never sits in memory.

Status Code When
200, 206 The bytes.
404 not_found Unknown or invalid id.
404 output_unavailable The asset is known but its bytes are gone, from disk or from ComfyUI. Re-run the job with a changed prompt.
405 method_not_allowed A method other than GET or HEAD.
500 upstream_error ComfyUI could not be reached.
502 upstream_error ComfyUI answered with another status.

Delete and expiry

DELETE /api/v2/assets/{id} removes the record from the index and answers 204. An unknown id is 404 not_found. The file itself stays where it is, in ComfyUI's directories or under the base directory.

An asset with expires_at is removed from the index once that time passes. A sweep runs every 300 seconds, or every COMFY_ASSET_CLEANUP_INTERVAL seconds when that variable holds a positive integer. It runs with and without --state-dir, and expires_at persists in the state database. Like a delete, the sweep removes records, not files.

Output asset ids

Every output on a job carries a stateless id:

base64url(payload) "." base64url(HMAC-SHA256(secret, base64url(payload)))

The payload is a JSON object naming the job, filename, subfolder, and ComfyUI folder type:

{"j": "813f058f-80fc-4023-82bf-8c20fe3190ca", "f": "out.png", "s": "", "t": "output"}

Both parts use unpadded base64url. The job id inside the signed payload means two jobs that write the same filename get different ids, so a client holding one job's URL is never served another job's bytes. The signature means a client cannot forge an id naming an arbitrary file that /view can reach. Any id that fails verification, including a well-formed forgery or a single changed character, reads as 404 not_found.

The payload is serialized exactly as Python's json.dumps writes it, so this binary and the Python proxy mint the same id for the same output and secret.

A value output gets an id of the same form whose payload names the job, node, key, and position in the list ({"j", "n", "k", "i"}) in place of a file. It is signed with the same secret, carries no f, and so never decodes as a file id or the other way round. Its content is read back from ComfyUI's history on each request. The proxy keeps no copy.

The secret is 32 random bytes made at startup. With --state-dir, it is stored in the database and reused, so ids minted before a restart still verify. Without it, a restart makes a new secret, and earlier output ids read as not found. See State and service.

core/ASSET references

A workflow input can name an asset instead of a filename. On submit, the proxy replaces each reference with the value ComfyUI's loaders expect:

{"__type": "core/ASSET", "info": {"id": "31524f5f-055f-4bfc-9d5e-2418342382c2"}}
info Resolves to
id of an uploaded input The ComfyUI filename, with its subfolder
id of a placed or registered model The root-relative file_path
id of a job output The output's filename, with its subfolder
hash (used when id is absent or does not resolve) The asset that owns that hash

A reference that does not resolve, or has no info object, fails the submit with 422 missing_asset and lists it in details.missing_ids. See Submit a job.