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

# 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`.




## OpenAPI

````yaml /openapi-v2.yaml post /api/v2/jobs
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/jobs:
    post:
      tags:
        - jobs
      summary: Submit a workflow for execution
      description: |
        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`.
      operationId: postJobs
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - workflow
              properties:
                workflow:
                  type: object
                  description: API-format workflow graph, verbatim.
                  additionalProperties: true
                extra_data:
                  type: object
                  description: >-
                    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, never persisted, and excluded from
                    idempotency comparison.
                  additionalProperties: false
                  properties:
                    api_key_comfy_org:
                      type: string
                      description: API key for partner (API) nodes.
      responses:
        '201':
          description: Job created and queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Job'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '402':
          description: '`insufficient_credits` (Cloud / serverless only).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          description: >-
            `invalid_workflow` (with per-node details), `workflow_format_ui`,
            `missing_asset`, or `idempotency_key_reuse`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: >-
            `queue_full` (bounded queue depth reached) or, on deployment-scoped
            surfaces, `deployment_not_ready` (deployment still
            provisioning/starting). Disambiguate by `error.code`; both mean back
            off and retry after `Retry-After`.
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '500':
          $ref: '#/components/responses/UpstreamError'
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:
    Job:
      type: object
      description: >-
        One execution of a workflow. Durable from creation until `expires_at`;
        `outputs` populates incrementally during execution.
      required:
        - id
        - status
        - created_at
        - started_at
        - completed_at
        - expires_at
        - queue_position
        - progress
        - outputs
        - error
        - urls
      properties:
        id:
          type: string
          example: 7f3d2c1b-9a8e-4d6f-b012-3c4d5e6f7a8b
        status:
          $ref: '#/components/schemas/JobStatus'
        created_at:
          type: string
          format: date-time
        started_at:
          type: string
          format: date-time
          nullable: true
        completed_at:
          type: string
          format: date-time
          nullable: true
        expires_at:
          type: string
          format: date-time
          description: Retention deadline — a platform property, not an API constant.
        queue_position:
          type: integer
          nullable: true
        progress:
          allOf:
            - $ref: '#/components/schemas/Progress'
          nullable: true
          description: The latest progress snapshot; same data the SSE stream pushes.
        outputs:
          type: array
          items:
            $ref: '#/components/schemas/Output'
        error:
          allOf:
            - $ref: '#/components/schemas/JobError'
          nullable: true
        logs:
          allOf:
            - $ref: '#/components/schemas/JobLogs'
          description: >-
            What the run printed. **Only jobs run on the serverless platform**
            (a `{deployment}.run.comfy.app` host) carry it. Comfy Cloud and
            self-hosted callers never receive it, so on those surfaces the field
            is always absent and a client should not wait for one. Where it is
            populated it is captured for every job, success and failure alike,
            since a job that succeeds while producing the wrong thing is exactly
            what a failure-only log cannot explain. It lives as long as the job
            it belongs to: nothing ages it out ahead of the job's own
            `expires_at`, so a job never outlives its log. **Absent, not null**,
            when there is none: the surface does not populate it at all, the job
            has not finished, the job predates log capture, or the job ran on
            the public demo deployment, which captures and stores the log like
            every other serverless deployment but withholds it on read, because
            that surface takes callers with no credential and a job id would
            otherwise be the only thing between one anonymous caller and
            another's run. Those cases are deliberately not distinguished,
            because a caller's next action is the same in all of them, which is
            to stop expecting a log. Returned by `GET /api/v2/jobs/{id}` only.
            It is deliberately absent from the job object on `POST
            /api/v2/jobs`, on `POST /api/v2/jobs/{id}/cancel`, and on the SSE
            `status` event: the last is pushed on every transition to every open
            stream, and a log on each frame would pay for the whole thing
            repeatedly to deliver it once. A client that streams to a terminal
            status and wants the log re-reads the job.
        metrics:
          type: object
          description: >-
            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.
          additionalProperties:
            type: integer
            nullable: true
          example:
            queue_ms: 9000
            execution_ms: 42000
        urls:
          $ref: '#/components/schemas/JobUrls'
    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),
        `queue_full` (429 + Retry-After), `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) and
        `deployment_stopped` (422 — terminal deployment state; a retry
        cannot succeed without operator action). 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'
            details:
              type: object
              nullable: true
              additionalProperties: true
              example:
                node_errors:
                  '12':
                    - field: model
                      reason: missing_input
    JobStatus:
      type: string
      enum:
        - queued
        - running
        - succeeded
        - canceling
        - canceled
        - failed
        - expired
      description: |
        Lifecycle: queued → running → succeeded | failed | expired;
        a cancel request moves running → canceling → canceled.
        Terminal states: succeeded, canceled, failed, expired.
    Progress:
      type: object
      description: >-
        Server-computed progress snapshot (node-count and sampler-step
        weighted). Complete per snapshot — one fully re-syncs a client.
      required:
        - value
        - nodes_done
        - nodes_total
      properties:
        value:
          type: number
          format: double
          minimum: 0
          maximum: 1
          description: Overall fraction, server-computed.
          example: 0.42
        nodes_done:
          type: integer
          example: 11
        nodes_total:
          type: integer
          example: 31
        current_node:
          type: string
          nullable: true
          example: '12'
        current_node_class:
          type: string
          nullable: true
          example: KSampler
        step:
          type: integer
          nullable: true
          example: 21
        steps:
          type: integer
          nullable: true
          example: 50
        message:
          type: string
          nullable: true
          example: KSampler 21/50
    Output:
      type: object
      description: >-
        A committed job output. Outputs are assets: `id` is the asset UUID,
        retrievable via GET /api/v2/assets/{id} for as long as the job is
        retained. `hash` is lazily computed and may be null on the retrieval hot
        path.
      required:
        - node_id
        - name
        - type
        - content_type
        - size_bytes
        - id
        - hash
        - url
        - url_expires_at
      properties:
        node_id:
          type: string
          example: '9'
        name:
          type: string
          example: ComfyUI_00001_.png
        type:
          $ref: '#/components/schemas/OutputType'
        content_type:
          type: string
          example: image/png
        size_bytes:
          type: integer
          format: int64
          example: 1848320
        id:
          type: string
          description: Asset UUID.
          example: 9f8a1c0d-2b3e-4f56-...
        hash:
          type: string
          nullable: true
          description: '`blake3:<hex>`; null until lazily computed.'
        url:
          type: string
          format: uri
        url_expires_at:
          type: string
          format: date-time
        job_id:
          type: string
          nullable: true
          description: ID of the job that produced this output.
    JobError:
      type: object
      description: Execution failure detail, carried in `job.error` (not an HTTP error).
      required:
        - code
        - message
      properties:
        code:
          type: string
          example: node_execution_error
        message:
          type: string
        node_id:
          type: string
          nullable: true
        class_type:
          type: string
          nullable: true
        traceback:
          type: string
          nullable: true
    JobLogs:
      type: object
      description: >-
        A job's captured execution log. Diagnostics, not a contract on content:
        this is whatever the workflow's own code and nodes wrote to standard
        output, in the order they wrote it, so nothing about its shape is stable
        between runs or between releases of a build. It is **untrusted text** —
        a workflow chooses what goes in it — and must be rendered as plain text
        rather than interpreted.
      required:
        - text
        - truncated
        - captured_at
      properties:
        text:
          type: string
          description: The captured output.
        truncated:
          type: boolean
          description: >-
            `text` is the TAIL of a longer run. Implementations bound what they
            capture and store, so a workflow that prints megabytes keeps its
            last lines — where a failure normally is — instead of being dropped
            whole. True with an empty `text` means the log was captured and then
            shed entirely to fit.
        captured_at:
          type: string
          format: date-time
          description: When the run's output was read back off the worker.
    JobUrls:
      type: object
      description: >-
        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.
      required:
        - self
        - events
        - cancel
      properties:
        self:
          type: string
          format: uri-reference
        events:
          type: string
          format: uri-reference
        cancel:
          type: string
          format: uri-reference
    OutputType:
      type: string
      enum:
        - image
        - video
        - audio
        - text
        - file
        - latent
      description: Normalized output kind — nothing silently dropped.
  responses:
    Unauthorized:
      description: '`unauthorized` — missing or invalid credentials.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
    Forbidden:
      description: '`forbidden` — authenticated but not allowed.'
      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'
  headers:
    RetryAfter:
      schema:
        type: integer
      description: Seconds to wait before retrying.
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        `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.

````