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

# Read one partner model's input and output schemas as an OpenAPI document.

> The per-model input and output schemas for a single Comfy Router model, served as a standalone OpenAPI document, so a caller - an SDK, a codegen tool, or an agent - can discover a model's arguments, and the shape of what it returns, without reading Comfy's prose docs. It is the discovery mechanism the SDK quickstart depends on.



## OpenAPI

````yaml /router-openapi.yaml get /v2/models/{provider}/{model}/openapi.json
openapi: 3.0.2
info:
  title: Comfy Router
  description: >-
    Comfy Router's public contract: the model catalog and the model-ID-addressed
    invocation routes, with the error buckets they return. Projected from the
    canonical Comfy API contract.
  version: '1.0'
servers:
  - url: https://api.comfy.org
security: []
tags:
  - name: Comfy Router
    description: Comfy Router's canonical, model-ID-addressed routes.
paths:
  /v2/models/{provider}/{model}/openapi.json:
    get:
      tags:
        - Comfy Router
      summary: >-
        Read one partner model's input and output schemas as an OpenAPI
        document.
      description: >-
        The per-model input and output schemas for a single Comfy Router model,
        served as a standalone OpenAPI document, so a caller - an SDK, a codegen
        tool, or an agent - can discover a model's arguments, and the shape of
        what it returns, without reading Comfy's prose docs. It is the discovery
        mechanism the SDK quickstart depends on.
      operationId: getRouterModelInputSchema
      parameters:
        - $ref: '#/components/parameters/RouterProvider'
        - $ref: '#/components/parameters/RouterModel'
        - in: header
          name: If-None-Match
          required: false
          description: >-
            The `ETag` a caller holds from an earlier `200`. When it matches the
            current document (RFC 9110 weak comparison; `*` matches any current
            document) the answer is a bodyless `304` carrying the same `ETag`,
            otherwise the full document.
          schema:
            type: string
      responses:
        '200':
          description: >-
            OK - the model's input and output schemas, as a standalone OpenAPI
            document.
          headers:
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
            ETag:
              $ref: '#/components/headers/RouterSchemaETagHeader'
            Cache-Control:
              $ref: '#/components/headers/RouterSchemaCacheControlHeader'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RouterModelInputSchemaDocument'
        '304':
          description: >-
            Not Modified - the document is unchanged since the `ETag` the caller
            sent in `If-None-Match`. No body is returned.
          headers:
            X-Comfy-Request-Id:
              $ref: '#/components/headers/RouterRequestIdHeader'
            ETag:
              $ref: '#/components/headers/RouterSchemaETagHeader'
            Cache-Control:
              $ref: '#/components/headers/RouterSchemaCacheControlHeader'
        '401':
          $ref: '#/components/responses/RouterRequestError'
        '403':
          $ref: '#/components/responses/RouterRequestError'
        '404':
          $ref: '#/components/responses/RouterRequestError'
        '500':
          $ref: '#/components/responses/RouterRequestError'
        '503':
          $ref: '#/components/responses/RouterRequestError'
      security:
        - BearerAuth: []
        - ApiKeyAuth: []
components:
  parameters:
    RouterProvider:
      name: provider
      in: path
      required: true
      description: >-
        Lowercase provider segment of the canonical `{provider}/{model}` model
        ID - the partner whose model is being run.
      schema:
        $ref: '#/components/schemas/RouterProviderSegment'
    RouterModel:
      name: model
      in: path
      required: true
      description: >-
        Lowercase model segment of the canonical `{provider}/{model}` model ID -
        the model to run within that provider.
      schema:
        $ref: '#/components/schemas/RouterModelSegment'
  headers:
    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.
      required: true
      schema:
        type: string
        format: uuid
        example: 6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21
    RouterSchemaETagHeader:
      description: >-
        Strong entity tag over the served document's bytes, for `GET
        /v2/models/{provider}/{model}/openapi.json`. A per-model schema changes
        rarely and an SDK re-fetches it often, so a caller should store this
        value and send it back as `If-None-Match` to get a `304` instead of the
        document.
      required: true
      schema:
        type: string
        example: '"6b8c1f2e0a9d4c3b5e7f8a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f"'
    RouterSchemaCacheControlHeader:
      description: >-
        Freshness directives for the served schema document. `private` because
        the route is authenticated - the document itself is not caller-specific,
        but a shared cache must not hold a response to an authenticated request
        - and `must-revalidate` so a stale copy is revalidated against the
        `ETag` rather than served on.
      required: false
      schema:
        type: string
        example: private, max-age=300, must-revalidate
    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'
  schemas:
    RouterModelInputSchemaDocument:
      type: object
      description: >-
        A standalone OpenAPI document describing one Comfy Router model's input
        and output - the body `POST /v2/models/{provider}/{model}` accepts for
        that model, under the operation's `requestBody`, and the body it
        returns, under that operation's `200` content. It is what `GET
        /v2/models/{provider}/{model}/openapi.json` returns. The component keeps
        its historical name, which predates the output half; the shape it
        describes is the whole document, not the input alone.
      additionalProperties: true
    RouterProviderSegment:
      type: string
      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.
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      maxLength: 64
      example: bfl
    RouterModelSegment:
      type: string
      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`.
      pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
      maxLength: 128
      example: flux-2-pro
    RouterErrorResponse:
      type: object
      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:
          type: string
          description: >-
            Human-readable description of the failure, safe to surface to an end
            user. Not machine-parsed - branch on `error_type` instead.
        error_type:
          $ref: '#/components/schemas/RouterErrorType'
        upstream_detail:
          type: string
          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.
        refusal_subject:
          type: string
          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; a provider whose refusal does not say
            which side it was about leaves it absent. It mirrors the
            `X-Comfy-Refusal-Subject` header.
      required:
        - detail
        - error_type
    RouterErrorType:
      type: string
      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.
      example: invalid_input
      x-comfy-error-types:
        - value: invalid_input
          tier: request
          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.
        - value: content_policy_violation
          tier: request
          meaning: >-
            The provider refused the request on content-policy grounds. The
            refusal is deterministic: re-sending the same input will be refused
            again.
        - value: provider_error
          tier: request
          meaning: >-
            The partner provider reported a failure of its own, or returned a
            response Router could not interpret as a result.
        - value: provider_timeout
          tier: request
          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.
        - value: insufficient_credits
          tier: request
          meaning: The calling workspace does not have enough credits to run the model.
        - value: model_not_found
          tier: request
          meaning: >-
            The `{provider}/{model}` ID names no model Router can run; an
            unknown provider lands here too. `detail` carries up to three
            suggestions drawn from the models the caller is entitled to see.
            This includes a catalogued ID the provider does not currently serve
            for Comfy; the response then carries no suggestions.
        - value: unauthorized
          tier: transport
          meaning: The request carried no usable credential.
        - value: forbidden
          tier: transport
          meaning: >-
            The credential is valid but is not entitled to this model or this
            operation.
        - value: concurrency_limit_exceeded
          tier: transport
          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.
        - value: client_disconnected
          tier: transport
          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.
        - value: internal_error
          tier: transport
          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.
        - value: deadline_exceeded
          tier: transport
          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.
        - value: not_enabled
          tier: transport
          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.
        - value: service_unavailable
          tier: transport
          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".
        - value: rate_limited
          tier: transport
          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.
        - value: cancelled
          tier: transport
          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.
        - value: queue_timeout
          tier: transport
          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.
        - value: request_not_found
          tier: transport
          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.
        - value: queue_backlog_full
          tier: transport
          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.
  responses:
    RouterRequestError:
      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'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RouterErrorResponse'
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      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.
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      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.

````