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

# Collect the result of one submitted request.

> The collect endpoint. On a request that has finished successfully it returns the partner model's own native output, byte for byte what the synchronous route's `200` carries for the same model and the same input - so the two delivery modes produce one result shape and a caller can move between them without a second parser.
WHILE THE REQUEST IS STILL RUNNING this returns `202` carrying the SAME `RouterQueueStatusResponse` body the status read returns, with the same `Retry-After` hint. A client that only ever wants the result can therefore poll this one URL and branch on the status code, and never call the status read at all; a client that wants to watch progress without pulling the output down uses the status read. Both are supported deliberately, and they cannot disagree, because the `202` body IS the status body.
A REQUEST THAT FINISHED WITH A FAILURE - a provider error, a policy refusal, a cancellation the caller asked for - is returned in Router's request-level error envelope, `RouterErrorResponse`, under the status the synchronous route would have answered for that same failure, with the coarse bucket repeated on `X-Comfy-Error-Type`. That is the `default` response below. Collecting a failure is not itself an error in the read: the read worked, and what it found was a failed run. A client that wants the terminal state WITHOUT this branch reads the status endpoint, where the same run is `COMPLETED` with an `error_type` and no status code carries the failure.
WITH ONE EXCEPTION, AND IT IS ABOUT THE CHARGE RATHER THAN ABOUT THE CANCELLATION. The rule this route applies is "a recorded charge and a stored result", not "the cancel arrived late": when a terminal request carries BOTH, this route answers `200` with that result whatever terminal reason the request carries, because the caller has already paid for it. A cancellation the caller was charged nothing for is answered by the error envelope exactly as above, whether it arrived before the partner call or while one was in flight - a cancellation is not itself billable, so a run cancelled before its charge was metered stays unbilled AND unreadable, which is the pairing that leaves the caller whole. The STATUS read is unchanged either way: the run is `COMPLETED` with `error_type: cancelled`, so a client that needs to know a result was cancelled-but-delivered reads it there.
A RESULT DOES NOT LIVE FOREVER. Once its retention window has passed the read is `410`, permanently and for that id alone: the request really did exist and really did produce this answer, and saying so is materially different from `404`, which would tell a caller their id was never valid and send them to re-submit a generation they have already paid for.
This operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the synchronous route.



## OpenAPI

````yaml https://api.comfy.org/openapi get /v2/models/{provider}/{model}/requests/{request_id}
openapi: 3.0.2
info:
  title: Comfy API
  version: '1.0'
servers:
  - url: https://api.comfy.org
security: []
tags:
  - description: Comfy Router's canonical, model-ID-addressed routes.
    name: Comfy Router
  - description: >-
      Signed-in users running Comfy-owned serverless apps with free runs and
      flat or per-second pricing (FE-2737).
    name: App Proxy
paths:
  /v2/models/{provider}/{model}/requests/{request_id}:
    get:
      tags:
        - Comfy Router
      summary: Collect the result of one submitted request.
      description: >-
        The collect endpoint. On a request that has finished successfully it
        returns the partner model's own native output, byte for byte what the
        synchronous route's `200` carries for the same model and the same input
        - so the two delivery modes produce one result shape and a caller can
        move between them without a second parser.

        WHILE THE REQUEST IS STILL RUNNING this returns `202` carrying the SAME
        `RouterQueueStatusResponse` body the status read returns, with the same
        `Retry-After` hint. A client that only ever wants the result can
        therefore poll this one URL and branch on the status code, and never
        call the status read at all; a client that wants to watch progress
        without pulling the output down uses the status read. Both are supported
        deliberately, and they cannot disagree, because the `202` body IS the
        status body.

        A REQUEST THAT FINISHED WITH A FAILURE - a provider error, a policy
        refusal, a cancellation the caller asked for - is returned in Router's
        request-level error envelope, `RouterErrorResponse`, under the status
        the synchronous route would have answered for that same failure, with
        the coarse bucket repeated on `X-Comfy-Error-Type`. That is the
        `default` response below. Collecting a failure is not itself an error in
        the read: the read worked, and what it found was a failed run. A client
        that wants the terminal state WITHOUT this branch reads the status
        endpoint, where the same run is `COMPLETED` with an `error_type` and no
        status code carries the failure.

        WITH ONE EXCEPTION, AND IT IS ABOUT THE CHARGE RATHER THAN ABOUT THE
        CANCELLATION. The rule this route applies is "a recorded charge and a
        stored result", not "the cancel arrived late": when a terminal request
        carries BOTH, this route answers `200` with that result whatever
        terminal reason the request carries, because the caller has already paid
        for it. A cancellation the caller was charged nothing for is answered by
        the error envelope exactly as above, whether it arrived before the
        partner call or while one was in flight - a cancellation is not itself
        billable, so a run cancelled before its charge was metered stays
        unbilled AND unreadable, which is the pairing that leaves the caller
        whole. The STATUS read is unchanged either way: the run is `COMPLETED`
        with `error_type: cancelled`, so a client that needs to know a result
        was cancelled-but-delivered reads it there.

        A RESULT DOES NOT LIVE FOREVER. Once its retention window has passed the
        read is `410`, permanently and for that id alone: the request really did
        exist and really did produce this answer, and saying so is materially
        different from `404`, which would tell a caller their id was never valid
        and send them to re-submit a generation they have already paid for.

        This operation is deliberately tagged `Comfy Router` and NOT `API
        Nodes`, for the reason given on the synchronous route.
      operationId: GetRouterModelRequestResult
      parameters:
        - $ref: '#/components/parameters/RouterProvider'
        - $ref: '#/components/parameters/RouterModel'
        - $ref: '#/components/parameters/RouterQueueRequestId'
      responses:
        '200':
          content:
            '*/*':
              schema:
                format: binary
                type: string
            application/json:
              schema:
                $ref: '#/components/schemas/RouterModelOutput'
          description: >-
            OK - the result stored for a request that produced one - a request
            that completed successfully, or a terminal one that carries both a
            recorded charge and a stored result - returned unchanged under the
            partner's own media type, exactly as the synchronous route's `200`
            returns it, and under the same provider-selection contract the
            submit accepted: without `model_provider`, or with `model_provider`
            and `strict_mode=false` (the default, translated back into this
            model's native contract when possible, falling back to the alternate
            provider's own raw response on a translation failure - logged, never
            silent), the shape is this model's own native output; with
            `strict_mode=true` it is the alternate provider's response returned
            unchanged. For most models that is JSON (`RouterModelOutput`); for a
            model whose partner answers a generation directly as bytes it is
            those bytes under the partner's own `Content-Type`. Today no such
            model can be queued (the submit refuses it), so this branch is
            declared for the SDK contract ahead of the server serving it. A
            client must branch on the response `Content-Type` and must not
            assume a JSON document; the per-model contract is published at `GET
            /v2/models/{provider}/{model}/openapi.json`. This response carries
            `X-Content-Type-Options: nosniff`, so a partner media type is taken
            at its word and never sniffed into something else.
          headers:
            X-Comfy-Credits-Used:
              $ref: '#/components/headers/RouterCreditsUsedHeader'
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
            X-Content-Type-Options:
              $ref: '#/components/headers/RouterNoSniffHeader'
        '202':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouterQueueStatusResponse'
          description: >-
            Accepted - the request has not finished. The body is the same
            `RouterQueueStatusResponse` the status read returns, so this route
            can be polled on its own, and `Retry-After` hints when to ask again.
          headers:
            Retry-After:
              $ref: '#/components/headers/RouterQueuePollAfterHeader'
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
        '401':
          $ref: '#/components/responses/RouterRequestError'
        '403':
          $ref: '#/components/responses/RouterRequestError'
        '404':
          $ref: '#/components/responses/RouterRequestError'
        '409':
          $ref: '#/components/responses/RouterRequestError'
        '410':
          $ref: '#/components/responses/RouterRequestError'
        '422':
          $ref: '#/components/responses/RouterModelValidationError'
        '503':
          $ref: '#/components/responses/RouterRequestError'
        '504':
          $ref: '#/components/responses/RouterRequestError'
        default:
          $ref: '#/components/responses/RouterRequestError'
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
components:
  parameters:
    RouterProvider:
      description: >-
        Lowercase provider segment of the canonical `{provider}/{model}` model
        ID - the partner whose model is being run.

        The schema is `RouterProviderSegment`, the SAME component a catalog
        entry's `provider` field references, so an ID `GET /v2/models` lists
        cannot drift from the ids this route accepts. Its `pattern` is a
        CONTRACT statement, not enforcement: comfy-api installs no OpenAPI
        request validator and oapi-codegen binds path parameters as plain
        strings, so the handler must re-validate this segment itself before
        using it to select a provider or compose an upstream URL.
      in: path
      name: provider
      required: true
      schema:
        $ref: '#/components/schemas/RouterProviderSegment'
    RouterModel:
      description: >-
        Lowercase model segment of the canonical `{provider}/{model}` model ID -
        the model to run within that provider.

        As with `provider`, the schema is the shared `RouterModelSegment`
        component and its `pattern` documents the contract rather than enforcing
        it - see `RouterProvider`.
      in: path
      name: model
      required: true
      schema:
        $ref: '#/components/schemas/RouterModelSegment'
    RouterQueueRequestId:
      description: >-
        The queued request to address - the `request_id` the submission returned
        in its body. It is not the submission's `X-Comfy-Request-Id`: that
        header carries the id of one HTTP call and addresses nothing, as the
        `RouterQueueRequestId` schema spells out.

        It is scoped to the caller and to the `{provider}/{model}` in the same
        URL. An id that is real but was submitted to a different model is a
        `404` on every read here, not a redirect and not a silent success on the
        row the id names: the path parameters are redundant with the queued row,
        and this contract resolves that redundancy strictly so that one id has
        exactly one address.

        As with `provider` and `model`, the `format` documents the contract
        rather than enforcing it - comfy-api installs no OpenAPI request
        validator and oapi-codegen binds path parameters as plain strings, so a
        handler must re-validate this segment itself before using it to look a
        request up.
      in: path
      name: request_id
      required: true
      schema:
        $ref: '#/components/schemas/RouterQueueRequestId'
  schemas:
    RouterModelOutput:
      additionalProperties: true
      description: >-
        A partner model's native JSON output document, returned to the caller
        as-is. Its concrete shape is owned by the partner rather than by Comfy,
        so this is an open object: Router does not narrow, rename, or
        re-envelope the fields. It is a named component (never an inline
        anonymous object) because ComfyUI's spec-driven codegen needs a class to
        generate. For the concrete shape one model returns, read that model's
        own document at `GET /v2/models/{provider}/{model}/openapi.json`, whose
        `200` carries the per-model output schema when Comfy has described it.

        Declaring an object here is a codegen requirement, not a licence to
        re-decode the payload: numeric fidelity is the handler's job, since
        decoding into a Go `map[string]any` rounds integers above 2^53 (partner
        seeds and IDs) to `float64`. Forward the raw bytes rather than
        round-tripping them through a generic map. Body size is likewise bounded
        at the handler - comfy-api caps comparable bodies with
        `http.MaxBytesReader` - not by this schema.
      type: object
    RouterQueueStatusResponse:
      allOf:
        - $ref: '#/components/schemas/RouterQueueUrls'
        - $ref: '#/components/schemas/RouterQueueStatusFields'
      description: >-
        One queued request's current state, composed with the same three URLs
        the submission returned.
      type: object
    RouterProviderSegment:
      description: >-
        Lowercase `provider` segment of the canonical `{provider}/{model}` model
        ID - the partner whose model is being addressed. The invocation route's
        `provider` path parameter and a catalog entry's `provider` field both
        reference this one schema, which is what keeps the listed IDs and the
        accepted IDs from drifting apart.

        The `pattern` is a CONTRACT statement, not enforcement: comfy-api
        installs no OpenAPI request validator and oapi-codegen binds path
        parameters as plain strings, so a handler must re-validate the segment
        itself before using it to select a provider or compose an upstream URL.
        The alphabet deliberately admits no `/`, no percent-encoding, and no
        repeated separator, so no accepted value can contain a `.` or `..` path
        segment.
      example: bfl
      maxLength: 64
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      type: string
    RouterModelSegment:
      description: >-
        Lowercase `model` segment of the canonical `{provider}/{model}` model ID
        - the model to run within that provider. Shared by the invocation
        route's `model` path parameter and a catalog entry's `model` field, for
        the same no-drift reason as `RouterProviderSegment`.

        As with the provider segment, the `pattern` documents the contract and
        does not enforce it. Dots are permitted inside the segment because
        partner model IDs use them for versions (`flux-pro-1.1`), but a repeated
        separator is not, so `..` cannot appear.
      example: flux-2-pro
      maxLength: 128
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      type: string
    RouterQueueRequestId:
      description: >-
        Identifier of one queued Router request - the handle a caller polls,
        cancels and collects a result by.

        IT IS NOT THE VALUE OF `X-Comfy-Request-Id`, and the two must not be
        confused. This id is DURABLE: one value for the whole life of the queued
        request, returned in the submit response's `request_id` and embedded in
        every URL that response hands back. `X-Comfy-Request-Id` is the per-CALL
        id the server mints and re-asserts on every response - the one on that
        call's usage and audit event and on every log line for it - so a poll
        loop making twenty calls against one queued request sees twenty
        different header values and one unchanging `request_id`.

        READ THE HANDLE FROM THE BODY, never from the header. A client that
        keeps only the header polls a request that does not exist. The two ids
        are the two halves of an audit join rather than one value: quote the
        header to ask about ONE call and the charge it raised, quote
        `request_id` to ask about the QUEUED REQUEST across every call made
        against it.

        It is server-minted and is never read from a request. A caller-supplied
        handle would let two unrelated calls collide in the audit trail, which
        is worse than having no join at all. Use `Idempotency-Key` to make a
        re-submission safe; that is the caller-controlled identifier on this
        surface, and it is a different job.
      example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21
      format: uuid
      maxLength: 36
      pattern: ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
      type: string
    RouterQueueUrls:
      description: >-
        The three URLs that address the rest of one queued request's lifetime,
        returned on every response that carries a live handle so a client never
        composes a queue URL itself.

        Returning them is what keeps the path shape a Comfy implementation
        detail rather than a contract every SDK hard-codes: the flat
        `/v2/requests/{request_id}` form this contract deliberately left
        available stays a one-line change for clients that follow these fields,
        and a breaking one for clients that build the path from the model ID and
        the id.

        They are absolute URLs, on the same host the caller reached, and they
        carry no credential: the caller authenticates the follow-up read exactly
        as they authenticated the submission.
      properties:
        cancel_url:
          description: Absolute URL a cancellation is asked for at.
          example: >-
            https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/cancel
          format: uri
          type: string
        response_url:
          description: Absolute URL this request's result is collected from.
          example: >-
            https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21
          format: uri
          type: string
        status_url:
          description: Absolute URL of this request's status read.
          example: >-
            https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/status
          format: uri
          type: string
      required:
        - status_url
        - response_url
        - cancel_url
      type: object
    RouterQueueStatusFields:
      description: >-
        The half of `RouterQueueStatusResponse` that is not the URL block: one
        queued request's identity, its current state, and - when that state is
        terminal and the run did not succeed - the coarse bucket saying why.

        The composed response is the body of the status read's `200` AND of the
        result read's `202`, which is deliberate: a client that polls the result
        URL and a client that polls the status URL parse the same type, so the
        two views of one request cannot drift into two shapes.
      properties:
        error_type:
          allOf:
            - $ref: '#/components/schemas/RouterErrorType'
          description: >-
            Present only on a `COMPLETED` request that did not succeed, carrying
            the same coarse bucket the result read puts on `X-Comfy-Error-Type`
            when it returns that failure. It is what distinguishes a terminal
            request that succeeded from one that failed or was cancelled - there
            is no separate terminal status for either - and it is absent on
            success rather than null, so branch on its presence.

            It is the coarse bucket ALONE. The human-readable `detail`, and a
            model's per-field validation errors, are not restated here: the
            status read exists to be polled cheaply and repeatedly, and the
            result read returns the full error body once.
        queue_position:
          $ref: '#/components/schemas/RouterQueuePosition'
        request_id:
          $ref: '#/components/schemas/RouterQueueRequestId'
        status:
          $ref: '#/components/schemas/RouterQueueStatus'
      required:
        - request_id
        - status
      type: object
    RouterErrorResponse:
      description: >-
        Router's request-level error body: what is returned when the request
        never reached the model, or failed for a reason the model itself did not
        report - auth, quota, an unknown model ID, or provider transport. A
        model-level validation failure has its own shape,
        `RouterValidationErrorResponse`, because flattening a FastAPI `detail[]`
        array into this `detail` string would destroy the per-field granularity
        an SDK branches on.
      properties:
        detail:
          description: >-
            Human-readable description of the failure, safe to surface to an end
            user. Not machine-parsed - branch on `error_type` instead. For
            `model_not_found` on a retired model, `detail` reads `<id> was
            retired on <YYYY-MM-DD>; use <successor>` when a successor is
            recorded and visible to the caller, or just `<id> was retired on
            <YYYY-MM-DD>` when it is not, instead of the usual suggestion list.
            A successor named here is always a live catalog ID the call can be
            re-pointed at. A model whose sunset date is still in the future is
            not retired: it stays listed by `GET /v2/models` and stays runnable
            until that date. Like the rest of `detail`, this retirement wording
            is for display and carries no compatibility guarantee.

            Two documented exceptions to the not-machine-parsed rule, because
            `error_type` cannot express either: on `POST
            /v2/models/{provider}/{model}` the `409` is answered for three
            unrelated conditions that share the `invalid_input` bucket, and only
            `detail` separates them. A `detail` beginning `this request resolved
            a BYOK credential` is the BYOK `model_provider` refusal; a `detail`
            beginning "this request's `image` selects the edit operation" is the
            multipart `model_provider` refusal; neither clears with a new
            `Idempotency-Key`. Any other `invalid_input` `409` on that route is
            the key conflict. Both prefixes are stable contract terms and may be
            matched. Nowhere else does branching on `detail` carry a
            compatibility guarantee.
          type: string
        error_type:
          $ref: '#/components/schemas/RouterErrorType'
        refusal_subject:
          description: >-
            Which input or output a content-policy refusal was about, as a
            Router-level closed vocabulary: `input`, `output`, `input_text`,
            `input_image`, `input_video`, `input_audio`, `output_text`,
            `output_image`, `output_video`, `output_audio`. The bare `input` /
            `output` values name the side when the provider did not name a
            modality. Present only when `error_type` is
            `content_policy_violation` and the provider named the refused
            subject with a machine-readable code; absent otherwise. Never
            provider text. Named today for BytePlus, Runway, BFL, Gemini, Veo,
            Vertex, xAI and Wan refusals, and for the MiniMax Router's Nano
            Banana legs' Gemini refusals; a provider whose refusal does not say
            which side it was about leaves it absent. It mirrors the
            `X-Comfy-Refusal-Subject` header.
          type: string
        upstream_detail:
          description: >-
            A bounded, sanitized reason the model provider gave for rejecting
            the request, present only when `error_type` is `invalid_input` and
            `X-Comfy-Upstream-Status` is a provider `4xx` or `2xx` - i.e. the
            provider refused the request as malformed and said why. Usually that
            status is a `4xx`; it is a `2xx` for a provider that reports a
            rejected generation inside a success envelope (a BytePlus
            failed-task poll is HTTP `200` with the reason in its body). Absent
            on every other failure, including provider `5xx`, transport
            failures, content-policy refusals and any refusal Router raised
            about itself. It mirrors the `X-Comfy-Upstream-Detail` header.

            Unlike `detail`, this is provider-authored text, so it is
            length-capped and stripped of URLs, hostnames, emails, tokens and
            id-shaped values before it is surfaced; it is never the raw provider
            body. It is a human-readable hint, not machine-parsed - branch on
            `error_type` instead.
          type: string
      required:
        - detail
        - error_type
      type: object
    RouterValidationErrorResponse:
      description: >-
        Router's model-level `422` body, in the FastAPI form: the request was
        well-formed enough to reach the model and the model rejected its
        contents. Note it carries no `error_type` of its own - that is what
        `X-Comfy-Error-Type` on the response is for, so a client can read the
        coarse bucket off the header without first deciding which of the two
        Router error bodies it received.
      properties:
        detail:
          description: >-
            Every validation failure found on the request, one entry per
            offending field.
          items:
            $ref: '#/components/schemas/RouterValidationErrorDetail'
          type: array
      required:
        - detail
      type: object
    RouterErrorType:
      description: >-
        Coarse, machine-readable bucket for a Router failure, mirrored on the
        `X-Comfy-Error-Type` response header so a caller can branch without
        parsing the body. The set is closed at nineteen values: the six
        request-level buckets `invalid_input`, `content_policy_violation`,
        `provider_error`, `provider_timeout`, `insufficient_credits` and
        `model_not_found`, plus the transport-level `unauthorized`, `forbidden`,
        `concurrency_limit_exceeded`, `client_disconnected`, `internal_error`,
        `deadline_exceeded`, `not_enabled`, `service_unavailable`,
        `rate_limited`, `cancelled`, `queue_timeout`, `request_not_found` and
        `queue_backlog_full`. Closed describes the set as documented today, not
        a bound that holds forever: the set is expected to grow, which is why
        this is deliberately a plain string and not an `enum`, so a client must
        treat an unrecognised value as `internal_error` rather than switch
        exhaustively over the list above and break on the next addition.

        RETRY SEMANTICS. `not_enabled` is TERMINAL - the same request will be
        refused the same way, so a client must not retry it - while
        `service_unavailable` is TRANSIENT and SHOULD be retried with backoff.
        That opposition is the whole reason they are two buckets rather than
        one: a retry policy keys off the status/`error_type` pair, and a single
        shared bucket would make retry wrong in one direction or the other.

        `not_enabled` and `forbidden` both return `403` and, like the `504` pair
        below, the difference between them is the one the status cannot carry.
        `forbidden` means the credential is valid but is not entitled to this
        model or operation - a decision about the caller. `not_enabled` means
        Comfy Router is not switched on for the caller yet - a state of the
        product rollout, not a judgement about them, and one that changes
        without the caller doing anything. `404` would have claimed the model
        does not exist when it does, and a `5xx` would have blamed the server
        for a deliberate state, so neither was available as a way to separate
        the two by status instead.

        HOW OFTEN `not_enabled` IS SEEN CHANGES OVER TIME, AND IT NEVER STOPS
        BEING VALID. While Comfy Router is rolling out, most callers are not on
        the ramp yet and this is the ordinary answer for them. Once Router is
        fully rolled out it becomes rare. It does not become impossible: the
        deploy-time switch that turns Router off for an environment is a
        permanent operational lever, so `not_enabled` is still the correct
        answer the day it is pulled. It is therefore a value Comfy expects to
        STOP EMITTING often rather than one that is ever withdrawn - a published
        bucket is never removed, because removing it would delete the generated
        exception class an SDK built from it and break any client branching on
        it. `service_unavailable` has no such arc at all: a dependency can
        always fault.

        `deadline_exceeded` and `provider_timeout` both return `504` and the
        difference between them is which SIDE ran out of time, which is not
        cosmetic: `provider_timeout` means the upstream model provider ran out
        of time, while `deadline_exceeded` means COMFY stopped holding the
        connection at its own configured bound. A client that collapses the two
        onto the status alone loses that distinction, and with it the difference
        between "the partner is failing" and "the call is longer than the
        connection Comfy will hold".

        NEITHER BUCKET IS A STATEMENT ABOUT THE CHARGE. Comfy's charges settle
        on COMPLETION: a generation the provider completed is billed whether or
        not the caller was still connected to receive the response, and a
        generation that failed or never completed is not billed. Reaching
        `deadline_exceeded` or `client_disconnected` therefore does not tell a
        caller they were not charged.

        `cancelled` IS THE SAME KIND OF VALUE and it is the one most often read
        as a refund. Cancelling asks Comfy to stop waiting; it does not stop a
        partner that has already accepted the generation, so a request cancelled
        while it was still `IN_QUEUE` was never dispatched and cannot be
        charged, whereas one cancelled after it was admitted may still settle a
        charge. The bucket names what happened to the REQUEST, never what
        happened to the bill.

        It is deliberately a plain string rather than an `enum`: the set is
        expected to grow -- it already has, most recently with the
        queued-delivery buckets `cancelled`, `queue_timeout` and
        `request_not_found`, and `file_download_error` is named as a further
        addition -- and a generated client that hard-rejects an unrecognized
        bucket would fail hardest exactly when something has already gone wrong.
        Treat an unknown value as `internal_error`. Bucketing loses no
        granularity - the specific provider-level reason survives in
        `RouterValidationErrorDetail.type` and its `ctx`.
      example: invalid_input
      type: string
      x-comfy-error-types:
        - meaning: >-
            The request was rejected before it reached the model - a malformed
            body, a malformed or expired pagination cursor, an input the model's
            own schema does not accept, or an `Idempotency-Key` that cannot
            serve this request (already used for a different request - the
            method, the path and query, or the body differ - or already consumed
            by a call whose response cannot be replayed). Sent with `409` in the
            key cases and with `400`/`422` in the others; the status says which,
            and the key cases are the ones answered by using a new key rather
            than by editing the request.
          tier: request
          value: invalid_input
        - meaning: >-
            The provider refused the request on content-policy grounds. The
            refusal is deterministic: re-sending the same input will be refused
            again.
          tier: request
          value: content_policy_violation
        - meaning: >-
            The partner provider reported a failure of its own, or returned a
            response Router could not interpret as a result.
          tier: request
          value: provider_error
        - meaning: >-
            The partner provider did not answer within its deadline. This bucket
            is the provider timing out and never Router's own server deadline,
            which is reported as `deadline_exceeded` - the two share `504` and
            are separated because they name different causes: this one says the
            partner failed, that one says Comfy stopped holding the connection.
          tier: request
          value: provider_timeout
        - meaning: The calling workspace does not have enough credits to run the model.
          tier: request
          value: insufficient_credits
        - meaning: >-
            The `{provider}/{model}` ID names no model Router can run: an
            unknown provider, an unenrolled id, one the catalog has retired past
            its sunset date, or a catalogued ID the provider does not currently
            serve for Comfy. A retired id's `detail` reads `<id> was retired on
            <YYYY-MM-DD>; use <successor>` when a successor is recorded and not
            withheld, or just `<id> was retired on <YYYY-MM-DD>` when it is not,
            instead of a suggestion list; the successor named for a retired id
            is always a live catalog ID you can re-point the call at, and a
            successor is dropped from `detail` either because the table records
            none, or because the recorded one is currently withheld behind an
            embargo the caller cannot see. A model whose sunset is still in the
            future is not withdrawn: it stays listed by `GET /v2/models` and
            stays runnable until the date, so a listing taken today remains the
            answer to what runs today. Otherwise `detail` carries up to three
            suggestions drawn from the models the caller is entitled to see,
            except for a catalogued ID the provider does not currently serve for
            Comfy, whose response carries no suggestions.
          tier: request
          value: model_not_found
        - meaning: The request carried no usable credential.
          tier: transport
          value: unauthorized
        - meaning: >-
            The credential is valid but is not entitled to this model or this
            operation.
          tier: transport
          value: forbidden
        - meaning: >-
            The workspace already has as many calls in flight as it is allowed;
            retry once one of them finishes. It carries one further condition on
            the run route, on a `409` rather than the `429` above: another call
            is already in flight for the `Idempotency-Key` this request
            presented. Re-send the same key after `Retry-After` seconds to
            collect that call's result.
          tier: transport
          value: concurrency_limit_exceeded
        - meaning: >-
            The caller closed the connection before Router could return a
            result. It is logged rather than delivered - there is no socket left
            to write it to - and it is an attribution, not a billing outcome: a
            provider generation that completed is billed regardless of whether
            the caller received the response.
          tier: transport
          value: client_disconnected
        - meaning: >-
            Router itself failed. It is also the value a client should treat any
            unrecognized bucket as, so a later addition to the set does not
            break a client generated before it.
          tier: transport
          value: internal_error
        - meaning: >-
            Comfy stopped holding the connection at its own configured bound
            before an answer arrived. It shares `504` with `provider_timeout`
            and the pair says which side ran out of time; this one is Comfy's
            own bound, so nothing about the request was rejected and the same
            request may be retried. It says nothing about the charge: a provider
            generation that completed is billed regardless of whether the caller
            received the response. Retry it with the same `Idempotency-Key`:
            when the provider had already accepted the generation, the retry
            collects that generation rather than dispatching another, and a
            `Retry-After` on the `504` says when to ask.
          tier: transport
          value: deadline_exceeded
        - meaning: >-
            Comfy Router is not switched on for this caller yet. Nothing about
            the request is wrong and the model exists, which is why this is not
            `model_not_found`; it shares `403` with `forbidden` and is not the
            same thing, because `forbidden` is an entitlement decision about the
            caller while this is a state of the rollout. It is terminal: do not
            retry, and do not treat it as an outage. The one exception to "about
            the caller" is the queued submit, which also answers `not_enabled`
            for a model whose partner answers a generation directly as bytes:
            that model cannot yet be queued, so it is the model and not the
            caller that is refused, nothing is queued or charged, and the
            synchronous route `POST /v2/models/{provider}/{model}` runs it
            instead.
          tier: transport
          value: not_enabled
        - meaning: >-
            A service Comfy Router depends on is temporarily unavailable and the
            caller did nothing wrong. Retry it with backoff: it is the one
            bucket here whose condition clears on its own, without the caller
            changing the request and without a concurrency slot freeing, which
            is what distinguishes it from the other retryable answers
            (`concurrency_limit_exceeded`, `deadline_exceeded`). It is separate
            from `internal_error` - which is a `500` and means Router itself
            failed - so a client can tell "come back shortly" from "this call is
            not going to work".
          tier: transport
          value: service_unavailable
        - meaning: >-
            The caller has spent an allowance measured over a window and must
            wait for that window to roll. It shares `429` with
            `concurrency_limit_exceeded` and is not the same thing: that one
            clears the moment one of the caller's own in-flight calls finishes,
            so retrying in seconds is right, whereas nothing the caller does
            drains this one early. `detail` names the window.
          tier: transport
          value: rate_limited
        - meaning: >-
            A queued request was withdrawn — through the cancel route, or by an
            operator — before it produced a result; it is terminal, and it is
            not by itself a statement about the charge. Cancelling stops Comfy
            waiting and it does not always stop the partner working, so a
            request cancelled while it was still `IN_QUEUE` was never dispatched
            and cannot be charged, whereas one cancelled after it was admitted
            may still be charged — a partner generation that completes is
            charged whether or not anyone collected it, which is why the cancel
            route calls the ask a request rather than a guarantee. It is not
            `client_disconnected`: that one says nobody is listening any more
            while a generation may still be running and billable, whereas this
            says the request itself was withdrawn. A caller polling the status
            read sees `200` with this in the body, because the request ended and
            so the read succeeded. Collecting the result of such a request
            answers `409` in Router's error envelope: that read also worked, and
            what it found is a request whose terminal state - the caller's own
            decision - leaves nothing to return. It is deliberately not `410`,
            which on that route means the result aged out of its retention
            window, and not a `5xx`, which would report the caller's own
            cancellation as a Router fault and read to a polling client as worth
            retrying.
          tier: transport
          value: cancelled
        - meaning: >-
            A queued request waited past its queue timeout without ever being
            admitted. Terminal, unbilled, and it never took a concurrency slot —
            the job never reached a provider. Deliberately not
            `deadline_exceeded`, which is the synchronous route's connection
            bound: a `deadline_exceeded` generation may be running and billable,
            whereas this one provably never started. It is returned under `504`,
            which it shares with `provider_timeout` and `deadline_exceeded`
            because a status can only say that a clock ran out; which clock -
            the partner's, Comfy's connection bound, or the queue's admission
            bound - is what `error_type` carries. The request itself is
            terminal, so submit a new one rather than re-reading this one.
          tier: transport
          value: queue_timeout
        - meaning: >-
            The `request_id` names no request of the caller's under this model.
            It is the second of the two conditions the queued reads' `404`
            covers; the first is the `{provider}/{model}` ID resolving to no
            partner model, which is `model_not_found` and carries fuzzy model
            suggestions. It also covers the right-id / wrong-model URL the path
            shape refuses, and it is deliberately indistinguishable from a
            request in another workspace, so a probe with a guessed id learns
            nothing. A request that has merely aged out of its retention window
            is `410`, not this.
          tier: transport
          value: request_not_found
        - meaning: >-
            The caller already has too many queued requests waiting to run, so
            this submit was refused. It shares `429` with
            `concurrency_limit_exceeded` and is not the same thing: that one is
            the synchronous route's answer for too many calls in flight at once,
            whereas the queue accepts a submit at that limit and parks it, and
            this bucket is the separate bound on how many a caller may leave
            waiting so that parking cannot mean enqueuing without end. It clears
            as the caller's own queued requests finish, so retry once some of
            them complete.
          tier: transport
          value: queue_backlog_full
    RouterQueuePosition:
      description: >-
        How many requests are ahead of this one in the queue, at the instant the
        response was composed. Zero means this request is at the front.

        IT IS A SNAPSHOT AND NOT A PROMISE. It can move in either direction:
        other callers submit, requests ahead complete or are cancelled, and
        capacity is not shared equally between models. Show it to a human as a
        rough progress signal; do not compute an ETA from it and do not branch a
        retry policy on it - `Retry-After` is the number that exists for that,
        and `status` is the one that says whether the request is still waiting
        at all.

        It is absent once the request is no longer `IN_QUEUE`, rather than being
        reported as zero: zero already means "at the front", so reusing it for
        "no longer queued" would make a running request indistinguishable from
        the next one to start.
      example: 3
      minimum: 0
      type: integer
    RouterQueueStatus:
      description: >-
        The state of a queued Router request. It has exactly three values, and
        unlike `RouterErrorType` this one is a closed `enum`, because the two
        schemas are closed in opposite directions on purpose. `RouterErrorType`
        classifies failures and its set is expected to grow, so a generated
        client that hard-rejected an unrecognised bucket would fail hardest
        exactly when something had already gone wrong. This one is a lifecycle,
        and a lifecycle with a fourth state added later is a breaking change to
        every polling loop written against it whether it is declared as an enum
        or not - so it is declared as one, and the constraint is stated where a
        client can see it.

        THERE IS NO `FAILED` AND NO `CANCELLED`. A run that failed and a run the
        caller cancelled are both `COMPLETED`, carrying an `error_type`.
        `COMPLETED` means the request has reached a terminal state and will not
        change again; it does NOT mean it succeeded. A polling client's loop
        asks one question - "is it finished" - and gets one answer, instead of
        one per way a run can end; whether it succeeded is then a separate
        question answered by whether `error_type` is present.
      enum:
        - IN_QUEUE
        - IN_PROGRESS
        - COMPLETED
      example: IN_QUEUE
      type: string
    RouterValidationErrorDetail:
      description: >-
        One model-level validation failure, in the FastAPI form. `type` carries
        the specific provider reason - `value_error`, `missing`,
        `image_too_small`, `unsupported_audio_format`, `greater_than`,
        `file_too_large` and the rest - which is the granularity
        `RouterErrorType`'s coarse bucket cannot express. It is an open string
        and not an `enum` for the same reason: the provider vocabulary runs to
        roughly 48 values across two tiers and grows on the provider's release
        cycle, not ours, and an unmodelled value must reach the caller rather
        than fail deserialization.
      properties:
        ctx:
          $ref: '#/components/schemas/RouterValidationErrorContext'
        input:
          $ref: '#/components/schemas/RouterValidationErrorInput'
        loc:
          description: >-
            Path to the offending field, outermost segment first - for example
            `["body", "image_url"]`, or `["body", "images", 0]` where an integer
            indexes into an array.
          items:
            anyOf:
              - type: string
              - type: integer
          type: array
        msg:
          description: Human-readable description of this single failure.
          type: string
        type:
          description: >-
            Specific, machine-readable reason for this failure, passed through
            from the provider unchanged. This is the value a typed SDK exception
            hierarchy branches on; `error_type` on the response header is only
            its coarse bucket.
          example: image_too_small
          type: string
      required:
        - loc
        - msg
        - type
      type: object
    RouterValidationErrorContext:
      additionalProperties: true
      description: >-
        The violated bound for one `RouterValidationErrorDetail`, carried from
        the provider verbatim - for example `{"limit_value": 8}` alongside
        `greater_than`, `{"min_width": 512}` alongside `image_too_small`, or
        `{"max_size_bytes": 10485760}` alongside `file_too_large`. The key set
        is specific to the provider and the error type, so this is deliberately
        an open object: narrowing it to a fixed field list, or folding it into
        the `msg` string, is precisely how a ported integration compiles and
        then silently loses the branch that read the bound. Absent when the
        error type carries no bound.
      type: object
    RouterValidationErrorInput:
      description: >-
        The offending input value, echoed back verbatim so a caller can see what
        was rejected without re-deriving it from `loc`. Any JSON type - string,
        number, boolean, array, object or null - so this schema is deliberately
        left untyped rather than narrowed to an object. Absent when the provider
        does not echo the input back.
  headers:
    RouterCreditsUsedHeader:
      description: >-
        What this run cost, in Comfy credits, priced from the same rate card the
        charge itself is billed against - so a caller needs no price table of
        its own, and on a run that reached the provider through more than one
        billed call it is their sum rather than the last one. It reports a
        price, not a settled ledger entry. On a model Comfy bills while your
        request is still open, the value is published only once the usage event
        was accepted by billing; on a model that submits and then polls, it is
        the price the asynchronous worker will bill, recorded before that charge
        settles - so a run whose billing later fails can still have carried this
        header. Treat it as what you will be charged, not as proof you were, and
        reconcile against the usage and billing API rather than against this
        header alone. Absent whenever no cost was reported: a request billed
        against your own provider key, a partner whose response does not carry
        every dimension its price is computed from, a usage event that matched
        no billable metric, or a charge that did not reach billing - so a
        missing header means "not reported" and must not be read as "free", and
        a report that sums absent headers as zero will not reconcile. A run that
        was rated and genuinely cost nothing reads `0`, which is a reported cost
        rather than a missing one, so branch on whether the header is present
        rather than on whether its value is non-zero. Coverage is partial today
        and widening, so do not assume the header is present for every model. On
        a response that also carries `Idempotent-Replayed: true` this restates
        what the original run cost, and that run is charged once however many
        times you retry the key - so do not add the header up across retries of
        one `Idempotency-Key`. Absent on an error response: it is written only
        on the path that returns a result, which a refused call never reaches.
      schema:
        example: '12.5'
        type: string
    RouterRequestIdHeader:
      description: >-
        Server-generated identifier for this call, present on every Router
        response - success, 4xx and 5xx alike, because an error response is
        exactly when a user needs an id to quote in a support request. The same
        value is written into the call's usage/audit event, which is what lets a
        complaint about a charge be joined to the charge itself instead of
        searched for by timestamp.

        It is minted by the server and is never read from a request header of
        the same name: a caller-controlled id would let two unrelated calls
        collide in the audit trail, which would make the join actively
        misleading rather than merely absent. Sending this header on a request
        has no effect.
      required: true
      schema:
        example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21
        format: uuid
        type: string
    RouterNoSniffHeader:
      description: >-
        Always `nosniff`, on every successful run of a Router model.

        The 200 on `POST /v2/models/{provider}/{model}` carries the PARTNER's
        own `Content-Type`, echoed verbatim, and since the ElevenLabs audio
        operations were enrolled that body can be arbitrary bytes rather than a
        JSON document. A 2xx interstitial from a partner's CDN or WAF carrying
        `text/html` would otherwise be sniffed and rendered as a document on
        Comfy's own origin. This header is what stops that, so it is a
        REQUIREMENT of reading the response rather than a decoration: treat the
        declared `Content-Type` as the type, and do not infer one from the
        bytes.

        It is stamped unconditionally on the success, not only on the models
        classified as binary - a header present only where someone remembered to
        classify a family is a header absent on the next one.

        A Router error rendered FRESH does not carry it: those bodies are
        Router's own JSON under a media type Router chooses, which is not the
        case the header exists for. A RECORDED refusal replayed against an
        `Idempotency-Key` does carry it, because the replay path stamps it on
        every record it serves rather than restoring it from the record - so a
        `4xx` marked `Idempotent-Replayed: true` can carry it while the same
        refusal rendered fresh does not. Do not read its presence on an error as
        a signal about that response, and rely on it only where it is declared.
      required: true
      schema:
        enum:
          - nosniff
        example: nosniff
        type: string
    RouterQueuePollAfterHeader:
      description: >-
        Seconds to wait before polling this queued request again. It is Router's
        own estimate of when asking again is worth the round trip, and it moves
        with how far the request has actually got - a request at the back of the
        queue is told to wait longer than one already running.

        It is a HINT, not a bound: a read that arrives sooner is answered
        normally rather than refused. Honour it anyway. A client that polls
        faster learns nothing earlier and spends its own request-rate allowance
        doing it, and the allowance is what eventually refuses the read for
        real.
      schema:
        example: 3
        minimum: 1
        type: integer
    RouterErrorTypeHeader:
      description: >-
        Coarse, machine-readable bucket for the failure, set by Router on every
        error response. It carries the same value as
        `RouterErrorResponse.error_type`, and on the `422` it is the only
        machine-readable bucket, because that body is the FastAPI `detail[]`
        shape and has no `error_type` field of its own. A client can therefore
        branch on this header alone, before deciding which of the two Router
        error bodies it received.
      required: true
      schema:
        $ref: '#/components/schemas/RouterErrorType'
    RouterIdempotentReplayedHeader:
      description: >-
        Present and `true` when this response was served from an
        `Idempotency-Key`'s record rather than by running the model again. It
        carries the original call's status, body and content type, and it is not
        billed a second time - the charge settled when the original completed.
        The header is absent on a fresh run rather than sent as `false`, so
        branch on its presence.
      schema:
        example: true
        type: boolean
  responses:
    RouterRequestError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterErrorResponse'
      description: >-
        A Router request-level failure - the request never reached the model, or
        failed for a reason the model itself did not report. The body is
        `RouterErrorResponse` and the bucket is repeated on
        `X-Comfy-Error-Type`.
      headers:
        X-Comfy-Error-Type:
          $ref: '#/components/headers/RouterErrorTypeHeader'
        X-Comfy-Request-Id:
          $ref: '#/components/headers/RouterRequestIdHeader'
    RouterModelValidationError:
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterValidationErrorResponse'
      description: >-
        The request's contents were rejected against the model's schema. The
        body is `RouterValidationErrorResponse`, the FastAPI `detail[]` shape,
        so each offending field keeps its own specific `type` and `ctx`.
        `X-Comfy-Error-Type` carries the coarse bucket for the whole response. A
        JSON request body that is not an object at all (an array, a string, a
        number, `null`) is refused here too, as `dict_type` at `["body"]`.

        What this costs your `Idempotency-Key` depends on WHERE the rejection
        happened, judged on the same dispatch test every other status is judged
        on rather than on the status itself. Router validates against the
        model's own input schema BEFORE any provider call, and that refusal
        reached nothing: it is never charged, it does not consume the key, and
        the key is RELEASED - re-send the SAME key with the corrected body and
        the call runs for real, rather than replaying this `422` or colliding
        with it as a `409`. That is the common case, and it is called out
        because this is the one error a caller CAUSES, and therefore the one
        they are most likely to fix and re-send under the original key.

        On the synchronous run route (`POST /v2/models/{provider}/{model}`),
        Router's own pre-flight check of a provider's request shape, when it can
        name the offending field in the body you sent, answers here too, on the
        same terms as the schema check: nothing was dispatched, the key is
        released, and the response carries no `X-Comfy-Upstream-Status`, which
        is present only on an answer that reached the provider. The queued route
        does not report that check in this `detail[]` shape.

        A model that performs its OWN validation can also answer `422` on a call
        that really ran. That one DISPATCHED, so it is recorded and replayed
        like any other answer: a same-key retry is served the stored response,
        marked `Idempotent-Replayed: true`, and correcting the body requires a
        NEW key.
      headers:
        Idempotent-Replayed:
          $ref: '#/components/headers/RouterIdempotentReplayedHeader'
        X-Comfy-Error-Type:
          $ref: '#/components/headers/RouterErrorTypeHeader'
        X-Comfy-Request-Id:
          $ref: '#/components/headers/RouterRequestIdHeader'
  securitySchemes:
    BearerAuth:
      bearerFormat: JWT
      description: >-
        Bearer token authentication. Normally a Firebase or Cloud JWT. A
        'comfyui-' prefixed API key is also accepted in this header: the prefix
        classifies the value as an API key and it is validated exactly as if it
        had been sent in X-API-Key.

        Accepted on the operations served by the comfyFirebase auth middleware
        (BE-9720, parity with ingest).
      scheme: bearer
      type: http
    ApiKeyAuth:
      description: >-
        API key authentication. Send the key in the X-API-Key header; keys are
        prefixed with 'comfyui-' and are generated from user account settings.
        The same 'comfyui-' key is also accepted in Authorization: Bearer (see
        BearerAuth), and when both headers carry a key, X-API-Key wins.

        The comfyFirebase auth middleware checks this header before falling back
        to the Authorization bearer token, and it backs both the /customers and
        the /proxy authset entries.
      in: header
      name: X-API-Key
      type: apiKey

````

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