> ## Documentation Index
> Fetch the complete documentation index at: https://docs.comfy.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload an asset (single-call multipart)

> Single-call `multipart/form-data` upload. The platform streams the
bytes through its trusted byte-path, dedups by the server-computed
hash, and mints the asset record.

The blake3 hash is always computed server-side from the received
bytes — a client-declared `expected_hash` is verified, never trusted.
If location metadata (GPS, XMP, IPTC) is removed from an image before
it is stored, the asset's `hash` is the hash of the stored bytes and
can differ from `expected_hash`. On Comfy Cloud, an upload whose
bytes open as a JPEG, PNG, WebP or classic TIFF (including TIFF-based
camera RAW such as DNG, NEF, ARW and CR2, but not BigTIFF) and that
is over 50 MiB is refused with 422 `validation_error`, whatever its
`content_type` and whether or not it carries location metadata. A
request that declares a `Content-Length` over Comfy Cloud's 200 MB
(200,000,000-byte) body limit is refused before that check with a
plain 413 that does not use the `ErrorEnvelope` shape. A body sent
without a declared length is cut off when it passes that limit, and
no 413 is sent: a cut-off while the fields before `file` are read
fails with 422 `invalid_body`, and a cut-off inside the file usually
fails with 500 `upstream_error`. Nothing after the `file` field is
read, so a body that only passes the limit there is uploaded
normally.
Deployment-scoped surfaces neither strip location metadata nor apply
the 50 MiB image cap; they limit the request body to 110 MB
(110,000,000 bytes). A request that declares a `Content-Length` over
that limit is refused with the same plain 413; a body sent without a
declared length is cut off when it passes the limit, and the upload
then fails with an error response that is not guaranteed to be a 413.
The uploaded asset is referenceable immediately; any content scanning
runs in the background and does not block the response.

An upload is a single request. v1 expects files of up to ~100 MB;
the hard cap is the body limit of the surface it is sent to (above).
Chunked/resumable large uploads are a deliberate open question.

On Comfy Cloud an upload fails with 422 `upload_timed_out` when no
request bytes arrive for 30 seconds, or when the whole request takes
longer than 10 minutes, however steadily bytes keep arriving.
On Comfy Cloud an upload can also be refused with 503 `upload_busy`
and `Retry-After` when the server is busy with other uploads: nothing
was stored, so the same upload can be sent again after `Retry-After`.
On a Developer Platform deployment the same 422 `upload_timed_out`
applies when no request bytes arrive for 30 seconds, or when the whole
request takes longer than 20 minutes.




## OpenAPI

````yaml /openapi-v2.yaml post /api/v2/assets
openapi: 3.0.3
info:
  title: Comfy API v2
  version: 2.0.0
  description: |
    The official, versioned HTTP API for running ComfyUI workflows from
    external applications: upload inputs, submit a workflow, observe
    execution, retrieve results.

    Design principles:
    - **Poll-first.** Every capability is reachable via plain GET polling;
      the SSE stream is a live enhancement, never the source of truth.
    - **Everything is resumable.** Submission is idempotent; job state and
      outputs are retrievable by ID until `expires_at`.
    - **UUID identity, content-addressed dedup.** Assets are UUID-identified
      records over blobs keyed by a server-computed blake3 hash. The hash is
      nullable and may be computed lazily.
    - **Follow links, don't build URLs.** Responses embed follow-up URLs.

    Additive changes only within v2; breaking changes require v3.
servers:
  - url: http://127.0.0.1:8189
    description: Self-hosted (comfy-api-proxy)
  - url: https://cloud.comfy.org
    description: Comfy Cloud
  - url: https://{deployment}.run.comfy.app
    description: Serverless deployment
    variables:
      deployment:
        description: >-
          DNS-safe deployment id (subdomain label). Staging uses
          {deployment}.stg.run.comfy.app.
        default: dep-1234abcd-56ef-7890-abcd-ef1234567890
security:
  - bearerAuth: []
  - {}
tags:
  - name: assets
    description: UUID-identified records over content-addressed blobs.
  - name: jobs
    description: One execution of a workflow — durable, pollable, cancelable.
paths:
  /api/v2/assets:
    post:
      tags:
        - assets
      summary: Upload an asset (single-call multipart)
      description: |
        Single-call `multipart/form-data` upload. The platform streams the
        bytes through its trusted byte-path, dedups by the server-computed
        hash, and mints the asset record.

        The blake3 hash is always computed server-side from the received
        bytes — a client-declared `expected_hash` is verified, never trusted.
        If location metadata (GPS, XMP, IPTC) is removed from an image before
        it is stored, the asset's `hash` is the hash of the stored bytes and
        can differ from `expected_hash`. On Comfy Cloud, an upload whose
        bytes open as a JPEG, PNG, WebP or classic TIFF (including TIFF-based
        camera RAW such as DNG, NEF, ARW and CR2, but not BigTIFF) and that
        is over 50 MiB is refused with 422 `validation_error`, whatever its
        `content_type` and whether or not it carries location metadata. A
        request that declares a `Content-Length` over Comfy Cloud's 200 MB
        (200,000,000-byte) body limit is refused before that check with a
        plain 413 that does not use the `ErrorEnvelope` shape. A body sent
        without a declared length is cut off when it passes that limit, and
        no 413 is sent: a cut-off while the fields before `file` are read
        fails with 422 `invalid_body`, and a cut-off inside the file usually
        fails with 500 `upstream_error`. Nothing after the `file` field is
        read, so a body that only passes the limit there is uploaded
        normally.
        Deployment-scoped surfaces neither strip location metadata nor apply
        the 50 MiB image cap; they limit the request body to 110 MB
        (110,000,000 bytes). A request that declares a `Content-Length` over
        that limit is refused with the same plain 413; a body sent without a
        declared length is cut off when it passes the limit, and the upload
        then fails with an error response that is not guaranteed to be a 413.
        The uploaded asset is referenceable immediately; any content scanning
        runs in the background and does not block the response.

        An upload is a single request. v1 expects files of up to ~100 MB;
        the hard cap is the body limit of the surface it is sent to (above).
        Chunked/resumable large uploads are a deliberate open question.

        On Comfy Cloud an upload fails with 422 `upload_timed_out` when no
        request bytes arrive for 30 seconds, or when the whole request takes
        longer than 10 minutes, however steadily bytes keep arriving.
        On Comfy Cloud an upload can also be refused with 503 `upload_busy`
        and `Retry-After` when the server is busy with other uploads: nothing
        was stored, so the same upload can be sent again after `Retry-After`.
        On a Developer Platform deployment the same 422 `upload_timed_out`
        applies when no request bytes arrive for 30 seconds, or when the whole
        request takes longer than 20 minutes.
      operationId: postAssets
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - file
                - content_type
                - file_path
              properties:
                file:
                  type: string
                  format: binary
                  description: The raw bytes.
                content_type:
                  type: string
                  example: image/png
                file_path:
                  type: string
                  description: >-
                    Placement path / filename (global-namespace-root form, e.g.
                    `photo.png` or `models/checkpoints/x.safetensors`). On Comfy
                    Cloud the last segment becomes the asset name, so it must be
                    valid UTF-8, at most 255 bytes, must not start with `.`, and
                    must not contain control characters (C0, DEL, C1, U+2028,
                    U+2029) or invisible format and default-ignorable characters
                    (Unicode general category Cf, which includes the bidi
                    embedding, override and isolate controls, soft hyphen,
                    zero-width space, word joiner, BOM and tag characters, plus
                    the other default-ignorable code points such as U+034F,
                    U+2065 and the Hangul fillers); otherwise the request is
                    refused with 422 `invalid_body` before anything is stored.
                    ZWNJ/ZWJ (U+200C, U+200D) and the direction marks U+200E,
                    U+200F and U+061C are allowed, for right-to-left and emoji
                    names, and so are the visible Arabic number signs (prepended
                    concatenation marks such as U+0600) and variation selectors;
                    the direction marks can still change how a name is
                    displayed.
                  example: photo.png
                expected_hash:
                  type: string
                  description: >-
                    Optional client-computed blake3 (`blake3:<hex>`) of the
                    bytes sent. Verified against the server-computed blake3 of
                    the received bytes, not of the stored bytes; mismatch is
                    rejected with 409 `hash_mismatch`. On serverless deployments
                    no asset is minted and no existing asset's retention is
                    changed; elsewhere the upload is stored before the check, so
                    the 409 returns no asset and a retry with the corrected
                    expectation dedups onto it.
                  example: blake3:9f8a1c0d...
                tags:
                  type: array
                  items:
                    type: string
                  description: >-
                    Category tags (e.g. `input`). A create carries at most 64
                    tags, each a non-empty UTF-8 string of at most 256 bytes
                    (surrounding whitespace included) with no NUL byte. A
                    deployment adds `input` to every asset and counts it, so it
                    takes at most 63 of the caller's own; the `model_type:` tags
                    derived for a model asset are not counted. A tag set
                    breaking a limit is refused with 422. Cloud's hosted API
                    also caps this encoded JSON field at 8 KiB, so a long tag
                    set can be refused (422 `invalid_body`) below these limits.
                    Invalid UTF-8 or a lone surrogate escape (e.g. `\ud800`)
                    inside a JSON-encoded tag is not refused: JSON decoding
                    replaces it with U+FFFD (3 bytes each), and the 256-byte
                    limit applies to the replaced value.
                expires_in:
                  type: integer
                  minimum: 60
                  maximum: 604800
                  description: >-
                    Optional retention override in seconds (60s–7d): the asset's
                    `expires_at` becomes now + `expires_in`, replacing the
                    platform's default retention. Implementations without
                    configurable retention ignore it. The bounds apply to this
                    override only — the platform default is operator-configured
                    and may lie outside them.
                  example: 86400
      responses:
        '200':
          description: Bytes deduped to an existing blob; asset minted over it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '201':
          description: New blob stored; asset minted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Asset'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: '`hash_mismatch`.'
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '413':
          description: >-
            The upload is over the body limit. On Comfy Cloud (200 MB,
            200,000,000 bytes) and on deployment-scoped surfaces (110 MB,
            110,000,000 bytes) the request declared a `Content-Length` over it;
            the server's body limit sends this before the upload is handled, so
            the body is not an `ErrorEnvelope` and its shape is not part of this
            contract. The self-hosted proxy instead refuses a file over its
            configured cap (100 MiB by default) with an `ErrorEnvelope` whose
            code is `payload_too_large`.
        '422':
          description: >-
            `idempotency_key_reuse`, `input_blocked` (the bytes are already
            stored and flagged by content moderation, so `file_path` is not
            registered for them), `upload_timed_out` (the client stopped sending
            or sent too slowly; the server cannot recover the upload, so retry
            it from a faster connection), or validation failure — including a
            `tags` set over the limits above or a disallowed tag
            (`invalid_request` on a deployment or the self-hosted proxy; a
            deployment answers a body that is not `multipart/form-data` with a
            boundary with a plain 500 instead). On a deployment that includes a
            truncated `file` part: the body ends inside the part, before the
            delimiter that ends it. A body that stops right at that delimiter,
            without the final `--`, is not rejected: the part reads as complete
            and is stored. Comfy Cloud's asset upload answers validation failure
            instead with `invalid_content_type` (the request is not
            `multipart/form-data` with a boundary), `invalid_body` (a malformed
            multipart body or upload field) or `validation_error` (the platform
            refused the upload, for example an image over the size cap or a
            disallowed tag).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/UpstreamError'
        '503':
          $ref: '#/components/responses/DeploymentLookupUnavailable'
components:
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      schema:
        type: string
      description: >-
        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.
  schemas:
    Asset:
      type: object
      description: >-
        A user-owned record identified by a server-assigned UUID, backing an
        immutable blob whose content carries a server-computed blake3 hash.
        `hash` may be computed lazily: an asset record (and its retrievable
        bytes) can exist before its hash is filled in. On an output,
        `job_metadata` carries the labels its producing job was submitted with,
        so code holding only the asset knows which run it came from. It is
        absent for assets no job produced, for outputs of a job sent without
        labels, where the serving surface keeps no job labels, and when the
        labels could not be read: so `job_id` without `job_metadata` does not
        prove the job had no labels. A create whose file path and bytes match an
        existing output answers with that output, so its `job_id` and
        `job_metadata` are the output's.
      required:
        - id
        - hash
        - size_bytes
        - content_type
        - created_at
        - url
        - url_expires_at
      properties:
        id:
          type: string
          example: 9f8a1c0d-2b3e-4f56-8a7b-1c2d3e4f5a6b
        hash:
          type: string
          nullable: true
          description: '`blake3:<hex>`; null while lazily computed.'
          example: blake3:9f8a1c0d...
        size_bytes:
          type: integer
          format: int64
          example: 4816293
        content_type:
          type: string
          example: image/png
        file_path:
          type: string
          nullable: true
          example: photo.png
        created_new:
          type: boolean
          description: >-
            On create responses: distinguishes a brand-new blob (true) from a
            dedup hit against bytes the platform already had (false).
        created_at:
          type: string
          format: date-time
        url:
          type: string
          format: uri
          description: Short-lived content URL (signed, or proxy-served).
        url_expires_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          nullable: true
          description: >-
            Retention deadline for the asset itself (distinct from
            `url_expires_at`, the signed URL's validity). Null or absent means
            the asset is non-expiring. On a dedup-hit create response the
            deadline may be later than now + the requested/default retention:
            re-referencing content extends its retention, never shortens it.
        job_id:
          type: string
          nullable: true
          description: >-
            ID of the job that produced this asset. Absent for assets no job
            produced, such as a fresh upload.
        job_metadata:
          allOf:
            - $ref: '#/components/schemas/JobMetadata'
          description: >-
            The labels the producing job was submitted with, read through
            `job_id` on each read. Absent for assets no job produced, for
            outputs of a job sent without labels, where the serving surface
            keeps no job labels, when the job is gone, and when the labels could
            not be read: so `job_id` without `job_metadata` does not prove the
            job had no labels.
    ErrorEnvelope:
      type: object
      description: |
        Shared error envelope with machine-readable codes. Core codes (v1):
        `invalid_workflow` (422), `workflow_format_ui` (422),
        `missing_asset` (422), `hash_mismatch` (409), `blob_not_found`
        (404), `idempotency_key_reuse` (422), `upload_timed_out` (422: an
        asset upload stopped sending or was too slow; retry from a faster
        connection),
        `queue_full` (429 + Retry-After), `rate_limited` (429 + Retry-After:
        the caller is past a request rate limit; retry), `insufficient_credits`
        (402), `not_found` (404), `unauthorized` (401), `forbidden` (403).
        Deployment-scoped surfaces add: `deployment_not_ready` (429 +
        Retry-After — the deployment can still reach ready; retry),
        `deployment_unavailable` (429 + Retry-After: the deployment is ready
        but its GPU provider is not taking work on it yet; retry),
        `deployment_lookup_unavailable` (503 + Retry-After, on any
        operation: the deployment the address names could not be looked up
        just now; nothing was done, so retry), `database_unavailable` (503 +
        Retry-After, only on a job submit: the job could not be saved just
        now and nothing was started, so retry), `auth_unavailable` (503 +
        Retry-After, on any operation: the credential could not be checked
        just now, which says nothing about the credential, so retry),
        `output_unavailable` (503 + Retry-After, only on a job status read: an
        output of the job could not be prepared just now; the job is
        unchanged, so retry after `Retry-After`, and report the job if it
        keeps answering),
        `deployment_stopped` (422 — terminal deployment state; a retry
        cannot succeed without operator action), `invalid_request` (422:
        a malformed asset upload or asset-from-hash field, or a jobs-list
        `limit` that is not a positive integer), `validation_error` (422,
        Cloud: an asset upload or asset-from-hash field was refused, e.g. a
        tag over the tag limits), `content_blocked` (451:
        content moderation flagged the asset's bytes) and `sso_required` (403:
        the key is valid, but the account must sign in through its
        organization's single sign-on, which does not accept this key).
        Comfy Cloud's asset upload adds `invalid_content_type` (422: the
        request is not `multipart/form-data` with a boundary),
        `invalid_body` (422: a malformed multipart body or upload field),
        `validation_error` (422: the platform refused the upload, for example
        an image over the size cap or a disallowed tag) and `upload_busy`
        (503 + Retry-After: the server is busy with other uploads and nothing
        was stored, so retry).
        Job labels add: `metadata_invalid` (422: a submitted `metadata` breaks a
        limit; `details.key` names the key, except for too many pairs, which
        has no `details`), `metadata_not_supported` (422:
        this surface keeps no job labels yet), `invalid_metadata_filter` (400)
        and `invalid_cursor` (400). A surface that does not list jobs yet
        answers `not_implemented` (501). A 429 is disambiguated
        by `error.code` alone; clients should treat any 429 + Retry-After
        as "back off and retry".
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              example: invalid_workflow
            message:
              type: string
              example: 'Node 12 (KSampler): required input ''model'' is not connected'
            organization_id:
              type: string
              description: >-
                On `sso_required`: the organization whose single sign-on governs
                this key, the one that holds the account, else the one that
                holds the key's workspace. It is the `organization` query
                parameter of Comfy Cloud's single sign-on start; treat it as
                opaque. Absent when the organization is unknown, and on every
                other code.
              example: org_01HXYZEXAMPLE
            details:
              type: object
              nullable: true
              additionalProperties: true
              description: >-
                Machine-readable detail for the code. When it carries
                `node_errors`, that is keyed by node id and each value is a
                `JobNodeError`, the same shape as a job's `error.node_errors`,
                whether the refusal came at submit (for example
                `unknown_node_class`, a node class the deployment's build does
                not contain) or from ComfyUI after dispatch. With
                `unknown_node_class` errors it carries `unknown_node_classes`,
                the missing classes; with `waits_for_browser` errors,
                `browser_wait_node_classes`, the classes that wait for a
                browser. A submit refusal names at most fifty nodes. For
                `invalid_node`, `invalid_inputs` and `invalid_class_type`, when
                it found more than fifty, `node_errors_truncated` is true and
                `malformed_node_count` carries the full count; for
                `waits_for_browser`, `browser_wait_node_count` does. For
                `unknown_node_class` it names only nodes whose class is one of
                the at most ten listed in `unknown_node_classes`; when those
                nodes number more than fifty, `node_errors_truncated` is true
                and `unknown_node_count` is how many nodes have a listed class.
                Nodes whose class is past those ten are neither named nor
                counted.
              example:
                node_errors:
                  '12':
                    class_type: SomeCustomNode
                    errors:
                      - type: unknown_node_class
                        message: >-
                          this deployment's build does not contain the node
                          class SomeCustomNode.
                unknown_node_classes:
                  - SomeCustomNode
    JobMetadata:
      type: object
      description: >-
        The labels the job was submitted with, exactly as sent. Absent when it
        was sent with none.
      additionalProperties:
        type: string
      example:
        client: acme
  responses:
    Unauthorized:
      description: >-
        `unauthorized`: missing or invalid credentials. A credential that could
        not be checked answers `auth_unavailable` (503) instead.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: >-
        `forbidden` — authenticated but not allowed. On a serverless deployment,
        also `sso_required` — the key is valid, but the account must sign in
        through its organization's single sign-on, which does not accept this
        key; it can come back on any operation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    RateLimited:
      description: >-
        `rate_limited` — the caller has exceeded a request rate limit for this
        account. Account/rate-scoped, not resource-specific — this can be
        returned even for a job, asset or deployment the caller doesn't own or
        that doesn't exist.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    UpstreamError:
      description: >-
        `upstream_error` — an unexpected failure reaching or processing the
        request in this implementation's backing services. The message is always
        a generic, safe-to-display string; implementation detail (the specific
        upstream, its error text, transport failures) is never included here —
        see each implementation's own error-mapping notes. Every operation in
        this contract can fail this way.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    DeploymentLookupUnavailable:
      description: >-
        On deployment-scoped surfaces, `deployment_lookup_unavailable`: the
        deployment this address names could not be looked up just now. The
        request was refused before it did anything (nothing was created, read,
        changed or canceled), so the same request can be sent again after
        `Retry-After`. Every operation at a deployment's address can answer this
        way. A job submit can also answer this 503 with `database_unavailable`:
        the job could not be saved just now and nothing was started, so the same
        request can be sent again after `Retry-After`. No other operation
        answers `database_unavailable`. On Comfy Cloud an asset upload can also
        answer this 503 with `upload_busy`: the server is busy with other
        uploads and nothing was stored, so the same upload can be sent again
        after `Retry-After`. Any operation can also answer this 503 with
        `auth_unavailable`: the credential could not be checked just now, so the
        same request can be sent again after `Retry-After`. A job status read
        can also answer it with `output_unavailable`: an output of the job could
        not be prepared just now, and the job is unchanged. A transient storage
        failure clears on a later read after `Retry-After`; an output whose
        stored address is broken answers it on every read until the platform
        repairs the output.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before retrying.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        `Authorization: Bearer <credential>`. The credential is one of: an
        account-scoped API key (`comfyui-…`), accepted on Cloud and serverless;
        a Comfy Cloud session JWT; or an OAuth access token issued for the Comfy
        Cloud resource. Which kinds a given deployment accepts is deployment
        configuration — an API key always works on Cloud and serverless, and a
        deployment that does not accept JWT bearers answers `401` with a message
        saying so. Self-hosted accepts unauthenticated requests by default and
        can be configured with a static bearer token.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.