Submit a partner model run to the queue and return immediately.
Comfy Router’s queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below.
Choose it over the synchronous route when the model’s generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion.
ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key’s workspace, the workspace is unknown rather than absent, and the answer is a retryable 503service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402insufficient_credits, whatever their balance, because adding credits would not change the answer.
The 201 carries request_id and the three URLs that address the rest of this request’s lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move.
request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel.
READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand.
AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call’s own id in the header. Idempotent-Replayed: true marks exactly those responses.
So request_id is authoritative on every response. Keep the body field.
This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.
POST
/
v2
/
models
/
{provider}
/
{model}
/
requests
Submit a partner model run to the queue and return immediately.
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).
Encabezados
Idempotency-Key
string
Caller-generated key that makes retrying one logical call safe. A call that reached the caller with an answer is recorded against its key for 24 hours, and a retry carrying the same key is answered from that record instead of dispatching - and charging - the provider a second time, marked Idempotent-Replayed: true. Keys are scoped to the workspace your credential carries, or to your user when it carries none - so the keyspace is shared by every member of a workspace rather than private to one caller. Make a key unique across the whole workspace, not just within your own client: a second member who reuses a key string is answered from the first member's record, or refused 409 if the request differs. Because the scope follows the credential and not the person, a credential that carries no workspace at all scopes to your user id instead - so retrying one logical call under a different credential can land in a different namespace, where it is dispatched and charged again. Retry with the credential you started with. A keyed request with no authenticated caller is refused 401. The guarantee is a billing one: a key is charged at most once. It is not a promise that a key is dispatched at most once, and it does not make a lost call resumable. Some answers are recorded but not replayable for the full 24 hours, and the billing guarantee is the half that always holds: the key stays consumed - the retry never re-runs and never re-charges - but it is answered 409 invalid_input instead of being served the original body. That happens whenever Comfy does not hold a copy of the response it can still stand behind; a response past the replay size cap and a result addressed by an asset URL Comfy does not host are the two you are most likely to meet. The second is the one worth planning for, because it looks like an ordinary success: which models answer with a Comfy-hosted asset link, how long one stays valid, and what a result carries when an individual asset could not be copied are stated in one place, under Result assets in the API reference, and this paragraph does not restate them. On a model that returns its result on the original call, an answer still holding a partner's own asset link is replayed for a few minutes - which is where a dropped connection puts an SDK's automatic same-key re-send, and while the partner's link is certainly still alive - and refused after that rather than replayed dead. So a prompt retry of a partially re-hosted result behaves exactly like any other replay, and only a later one meets the 409. That short window is deliberately not offered on a model that submits and is polled, because there the partner may have minted the URL long before your call collected it and its remaining life is unknowable - and those models do not need it: a call cut off mid-generation keeps its key holding the generation, so the same-key retry collects the original result rather than a recorded copy of it. A response past the size cap has no window either and is refused from the start. The action on any of these 409 invalid_input refusals is the same: use a new key. Only an answer a provider actually produced is recorded, though. A refusal Router raises on its own before dispatching anything - not enabled for you yet (403), unknown model (404), not entitled to the model (403), a body the model's schema rejects or that names a different model than the path (422), a malformed request (400 invalid_input) - dispatched nothing and charged nothing, so it releases the key: re-send the same key once you are on the rollout ramp or have corrected the request and it runs for real, rather than replaying the refusal or colliding with it as a 409. That turns on whether a provider was reached, never on the status, so a 400 content_policy_violation - the partner's own answer to a call that ran, which some models meter - is recorded and replayed like any other answer. Releasing a refusal that dispatched nothing frees nothing chargeable, so it does not weaken the at-most-once billing guarantee above.
Generate a fresh key per logical call and reuse it across that call's retry attempts; a UUID is the intended shape. Persist the key BEFORE sending the request - a key held only in the memory of the process that crashed cannot be resent, and the retry that would have collected the result becomes a fresh, separately charged run.
A retry that arrives while the original is still in flight is answered 409 with Retry-After: wait and re-send the same key. The same key presented with a DIFFERENT request is a 409 too, rather than a silent overwrite - and "different" is judged on the whole request, not the body alone: the method, the path and query string, and the body must all match the original. One exception: a signed Comfy asset URL in the body (a Comfy-issued storage.googleapis.com URL, such as the download_url an input upload returns) is compared without its signature - the signing query parameters and any fragment - so a retry that re-signed the same input object still matches the original and collects its answer. Any other change to that URL, such as a different object or path, is a different request. A call whose recorded answer is an error is matched on the exact body instead, because the signature may be what failed: a re-signed retry of it is the different-request 409, and the remedy is a new key.
THE KEY IS NEVER RETURNED IN ANY RESPONSE - not on the 200, not on the 504, not in any error body. It is yours to mint and yours to keep: a caller who did not keep it has no way to collect a generation they were billed for. A deadline_exceeded504 on a generation the provider is still running answers a same-key retry with 504 and Retry-After again until it finishes, then with the result.
Omitting the header is allowed and leaves the call unguarded rather than refused: there is nothing to check, so every retry is a fresh dispatch and a fresh charge.
Required string length: 1 - 255
Ejemplo:
"6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21"
Parámetros de ruta
provider
string
requerido
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.
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.
Maximum string length: 64
Pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
Ejemplo:
"bfl"
model
string
requerido
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.
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.
Maximum string length: 128
Pattern: ^[a-z0-9]+([._-][a-z0-9]+)*$
Ejemplo:
"flux-2-pro"
Parámetros de consulta
model_provider
string
Selects an alternate provider to serve this model, instead of its current default. Omitting it runs native dispatch on the model's own default provider; default, comfy, and comfyui are aliases for that same native behavior and name no override, because Comfy Router is never itself a serving backend. The alternate providers Router can retarget a model onto are fal, wavespeed, runware, and higgsfield; which of them a given model supports is reported by GET /v2/models/{provider}/{model}. When an alternate provider is selected, the native request body is translated into that provider's real schema unless strict_mode=true; see strict_mode and fallback_provider. Its refusals are checked in a fixed order, and an earlier one answers whether or not a later one would. A value that is not a registered provider at all is refused 400 with error_type: invalid_input. Then a workspace configured for direct provider calls only refuses any non-default model_provider with 403 and error_type: forbidden, and Router never runs its fallback_provider retry for it; a resubmit carrying the Idempotency-Key of a request already accepted is answered as a replay of that request rather than refused. Then a request whose body selects a multipart operation (an edit - for example gpt-image's image field) is refused 409, also with error_type: invalid_input, whose detail begins "this request's image selects the edit operation" - but only when the named provider's translator for this model cannot itself serve that operation; a leg whose translator does carry the media is not refused and proceeds normally, so this is a per-leg refusal rather than a blanket one. Then a request that has already resolved a bring-your-own-key credential for the provider in the path is refused 409, also with error_type: invalid_input, whose detail begins this request resolved a BYOK credential - see the 409 on this route, where that detail is the only thing separating this case, and the multipart case above, from the Idempotency-Key one. That BYOK check runs whether or not the named provider has a leg for this model. Then the named provider's own gate refuses 403 with error_type: not_enabled when that provider is not turned on for you, and 503 with error_type: service_unavailable when the gate cannot be evaluated - a flag-evaluation failure, or a missing or nil gate entry. Only past all of those is a real provider that does not serve this model refused 400 with error_type: invalid_input, the same answer as a value that is not a registered provider at all - both mean the model_provider value cannot serve this model, and that 400's detail is human-readable and not a contract, so read GET /v2/models/{provider}/{model} to learn which providers a model does support rather than parsing it. Then, on a Seedance 2.x model, a body carrying an asset:// reference in any content[] image, video or audio url is refused 400 with error_type: invalid_input: those ids are BytePlus virtual-library assets no other provider can serve, so send the media itself as an http(s) URL or a data: URI to use model_provider. Past those 400s, this workspace's partner-provider policy for the named vendor is evaluated too and can refuse 403 or 503 of its own. Neither this parameter nor fallback_provider is available on a BYOK request, and the two are unavailable in different ways: the credential was resolved for the provider named in the path while every alternate leg dispatches on Comfy's own key, so an explicit model_provider is refused with that 409, and fallback_provider is inert rather than refused - no retry against an alternate provider is attempted and the first attempt's own failure is what the caller receives. The same split applies to a multipart body: only an explicit model_provider reaches the 409 above, and only for a leg whose translator cannot serve the operation, while the automatic on-failure retry fallback_provider controls is simply skipped (inert, not refused) for any multipart body. Router defines no provider_not_available or validation_errorerror_type: these conditions fold onto invalid_input. The error_type set can still grow, so treat any value you do not recognize as internal_error rather than switching exhaustively.
strict_mode
boolean
predeterminado:false
Only meaningful together with model_provider. false (the default): the request body must be this model's own native contract, translated to the alternate provider's real schema - any native field that cannot be expressed exactly is dropped and disclosed via the response's X-Comfy-Router-Dropped-Params header, never silently. true: the body must already be the alternate provider's own real schema, passed through unmodified in both directions - no translation, so X-Comfy-Router-Dropped-Params is never sent, and provider fallback is disabled for the call because a body shaped for one provider cannot be replayed against another (see fallback_provider).
reject_unknown_fields
boolean
predeterminado:false
Opt in to refusing a top-level request-body field this model's input schema does not declare, rather than accepting it; nested fields are not checked, and nothing is refused for a model whose schema allows undeclared fields or has no authored schema, or on a queued submit sent with strict_mode=true.
Omitted, or false (the default): unchanged behaviour. Most model input schemas here are open, so a field Comfy has not authored is accepted and forwarded, which is what lets a field a partner already supports reach it before Comfy documents it. The cost is that a misspelled field name, or a nested field written at the top level, is accepted too - and then dropped on the way to the provider, so the generation runs, and is billed, under the defaults that field was meant to change.
true: a top-level field the published input schema does not declare is answered 422 with an extra_forbidden entry naming it, before any provider call is made and before anything is billed. Only the top level is checked, and only names that match no declared property even case-insensitively - a field whose spelling differs from a declared one only by case is already refused (property_name_case) regardless of this parameter. A model with no authored input schema declares no properties, so it refuses nothing here.
What it refuses is a FLOOR, not a ceiling, in two stated cases. A model whose published schema sets additionalProperties itself is left exactly as it is: where that is false the schema already refuses undeclared fields with or without this parameter, and where it is true - as on the Anthropic and Gemini Interactions models, whose schemas promise fields such as tools, temperature and safety_settings pass through unchanged - the schema has declared that an undeclared field is allowed, and this parameter does not overrule it. And where a schema is a union (oneOf/anyOf), a field belonging to any branch is accepted, not only to the branch the body matched.
The parameter is per-request on purpose: closing every schema instead would start refusing bodies that succeed today, with no way for an existing caller to keep the behaviour they built against. It is accepted on both the synchronous POST and the queued POST .../requests submit, which validate the same body against the same schema. The one exception is strict_mode=true on the queued submit: that body is already in the alternate provider's own shape, so the queue does not validate it against this model's schema, and this parameter is accepted but has no effect there.
Cuerpo
application/json
The partner model's native JSON input, identical to the body the synchronous route accepts for this model, and it selects the operation and is metered the same way: the body's own fields choose which operation Router runs on a model that supports more than one (an input image switches an editable image model to image-to-image, which may be metered on its own rate; a Seedance first-frame image selects image-to-video and a reference image or clip selects reference-to-video). On native dispatch a Seedance request bills by the media kinds in content, not the roles: any video input bills as video-to-video, otherwise any image bills as image-to-video, otherwise text-to-video, except that a final render from a draft_task item bills as the operation of the Draft it renders, with some models charging the same rate for text-to-video and image-to-video; with model_provider selecting an alternate provider, each conditioned operation may be metered on that provider's own rate. The same provider-selection contract applies at dispatch: without model_provider, or with model_provider and strict_mode=false (the default), the body is the model's native document and is validated against the model's own input schema before the run is admitted, so a body the model would reject is a 422 here rather than a queued request that fails minutes later - a non-strict alternate-provider body is additionally translated into that provider's real schema at dispatch. With strict_mode=true the body must already be the alternate provider's own schema and is forwarded unchanged: native-schema validation is skipped, exactly as on the synchronous route (see model_provider and strict_mode).
A partner model's native JSON input document, forwarded to the provider 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.
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.
Respuesta
Created - the run was admitted to the queue. The body is the handle: the request_id, status: IN_QUEUE, a queue_position snapshot, and the status_url, response_url and cancel_url that address the rest of this request's lifetime. It is deliberately 201 and not 202: a queued request is a resource this call created and the three URLs address it, whereas the 202 on the result read below means "not ready yet, ask again" and creates nothing.
The handle returned when a run is admitted to the queue: the request's identity and state, composed with the three URLs that address the rest of its lifetime.
It is allOf: [RouterQueueUrls, ...] so that the URL half IS the same shape the status read returns rather than a copy of it, and a generated client can hand a submission to anything that accepts a queue handle.
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.
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.
Opciones disponibles:
IN_QUEUE,
IN_PROGRESS,
COMPLETED
Ejemplo:
"IN_QUEUE"
estimate
object
The pre-flight cost estimate for this request, present on a fresh admission when the cost estimate is enabled for the caller. It quotes the provider leg this request was pinned to (the queue has no fallback loop, so fallback_provider never changes the leg that runs). When no figure can be given it is still present, with source: unknown; the submission never fails because of the estimate. Omitted on an idempotent replay (Idempotent-Replayed: true), because the original quote is not stored. It is an estimate, not the final charge, and not a price lock: the run is charged at the rates in force when it is rated, and the settled charge in your workspace's usage and billing records is final.
Show child attributes
queue_position
integer
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.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"ComfyUI","colors":{"primary":"#49378B","light":"#F2FF59","dark":"#F2FF59"},"logo":{"light":"https://mintcdn.com/dripart/WCFdTagRdhtvk9-o/logo/light.svg?fit=max\u0026auto=format\u0026n=WCFdTagRdhtvk9-o\u0026q=85\u0026s=ec112de87239058f5a931a7e4b594965","dark":"https://mintcdn.com/dripart/QzWbjSCBG7w61rR3/logo/dark.svg?fit=max\u0026auto=format\u0026n=QzWbjSCBG7w61rR3\u0026q=85\u0026s=62b4d879fb3aadbd7a468d1539032d6f","href":"/"},"favicon":"https://mintcdn.com/dripart/maKDA164-rrTfyxY/favicon.ico?fit=max\u0026auto=format\u0026n=maKDA164-rrTfyxY\u0026q=85\u0026s=94f3a697cf4c9a1ee3961b69219d0942","appearance":{"default":"dark"},"navbar":{"links":[{"href":"https://blog.comfy.org/?utm_source=docs","label":"Blog"},{"href":"/tutorials/partner-nodes/pricing","label":"Pricing"},{"href":"https://platform.comfy.org/?utm_source=docs","label":"Platform"},{"href":"https://comfy.org/download?utm_source=docs","label":"Download"}],"primary":{"type":"button","label":"Comfy Cloud","href":"https://comfy.org/cloud?utm_source=docs"}},"footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"Resources","items":[{"label":"installation","href":"/installation/system_requirements"},{"label":"Tutorials","href":"/tutorials/basic/text-to-image"},{"label":"Development","href":"/development/overview"}]},{"header":"Products","items":[{"label":"Features","href":"https://www.comfy.org/?utm_source=docs"},{"label":"Gallery","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"Download","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"Company","items":[{"label":"About","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"Careers","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"Terms of Service","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"Privacy Policy","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"search":{"prompt":"Search Comfy documentation..."},"seo":{"metatags":{"googlebot":"max-video-preview:0"}},"icons":{"library":"fontawesome"},"integrations":{"ga4":{"measurementId":"G-MXC01ZDDHQ"},"posthog":{"apiKey":"phc_iKfK86id4xVYws9LybMje0h44eGtfwFgRPIBehmy8rO"}},"contextual":{"options":["copy","view","chatgpt","claude"],"display":"header"},"styling":{"latex":true},"navigation":{"languages":[{"language":"en","href":""},{"language":"zh","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"资源","items":[{"label":"安装","href":"/zh/installation/system_requirements"},{"label":"教程","href":"/zh/tutorials/basic/text-to-image"},{"label":"开发","href":"/zh/development/overview"}]},{"header":"产品","items":[{"label":"功能","href":"https://www.comfy.org/?utm_source=docs"},{"label":"画廊","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"下载","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"公司","items":[{"label":"关于","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"招聘","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"服务条款","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"隐私政策","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"href":"zh"},{"language":"ja","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"リソース","items":[{"label":"インストール","href":"/ja/installation/system_requirements"},{"label":"チュートリアル","href":"/ja/tutorials/basic/text-to-image"},{"label":"開発","href":"/ja/development/overview"}]},{"header":"プロダクト","items":[{"label":"機能","href":"https://www.comfy.org/?utm_source=docs"},{"label":"ギャラリー","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"ダウンロード","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"会社情報","items":[{"label":"概要","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"採用情報","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"利用規約","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"プライバシーポリシー","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"navbar":{"links":[{"href":"/ja/tutorials/partner-nodes/pricing","label":"料金"},{"href":"https://comfy.org/download?utm_source=docs","label":"ダウンロード"}],"primary":{"type":"button","label":"Comfy Cloud","href":"https://comfy.org/cloud?utm_source=docs"}},"href":"ja"},{"language":"ko","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"리소스","items":[{"label":"설치","href":"/ko/installation/system_requirements"},{"label":"튜토리얼","href":"/ko/tutorials/basic/text-to-image"},{"label":"개발","href":"/ko/development/overview"}]},{"header":"제품","items":[{"label":"기능","href":"https://www.comfy.org/?utm_source=docs"},{"label":"갤러리","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"다운로드","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"회사 정보","items":[{"label":"소개","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"채용","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"이용약관","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"개인정보 처리방침","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"navbar":{"links":[{"href":"/ko/tutorials/partner-nodes/pricing","label":"가격 정책"},{"href":"https://comfy.org/download?utm_source=docs","label":"다운로드"}],"primary":{"type":"button","label":"Comfy Cloud","href":"https://comfy.org/cloud?utm_source=docs"}},"href":"ko"},{"language":"es","href":"es"}]}},"deployment":{"deploymentMetadataValue":{"subdomain":"dripart","hasAudioNarration":false,"actualSubdomain":"dripart","customDomains":["docs.comfy.org"],"gitSource":{"type":"github","owner":"comfy-org","repo":"docs","deployBranch":"main","contentDirectory":"","isPrivate":false,"mountPath":"","label":""},"feedback":{"thumbs":true,"edits":true,"issues":true,"agentFeedback":true},"entitlements":{"AI_CHAT":{"status":"DISABLED"},"WEB_MCP":{"status":"ENABLED"},"NARRATION":{"status":"DISABLED"}},"buildId":"6af94007df163b1980475567","clientVersion":"0.0.4143","searchFilterCounts":[{"language":"en","tag":"Built-in Nodes","count":1183},{"language":"ja","tag":"組み込みノード","count":1183},{"language":"zh","tag":"内置节点","count":1183},{"language":"en","tag":"Nodes","count":1182},{"language":"ja","tag":"ノード","count":1182},{"language":"zh","tag":"节点","count":1182},{"language":"ko","tag":"내장 노드 (Built-in Nodes)","count":1141},{"language":"ko","tag":"노드","count":1140},{"language":"es","tag":"Nodos integrados","count":1075},{"language":"es","tag":"Nodos","count":1074},{"language":"en","tag":"Tutorials","count":427},{"language":"es","tag":"Tutoriales","count":427},{"language":"en","tag":"Model","count":382},{"language":"ja","tag":"Model","count":382},{"language":"ko","tag":"Model","count":382},{"language":"zh","tag":"Model","count":382},{"language":"es","tag":"Modelo","count":378},{"language":"en","tag":"Image","count":360},{"language":"en","tag":"Partner","count":359},{"language":"ja","tag":"Partner","count":359},{"language":"ko","tag":"Partner","count":359},{"language":"zh","tag":"Partner","count":359},{"language":"es","tag":"Partner","count":357},{"language":"es","tag":"Imagen","count":329},{"language":"ja","tag":"API 開発","count":326},{"language":"ko","tag":"API 개발","count":326},{"language":"zh","tag":"API 开发","count":326},{"language":"ja","tag":"はじめに","count":318},{"language":"ko","tag":"시작하기","count":318},{"language":"zh","tag":"开始使用","count":318},{"language":"en","tag":"Comfy Router","count":303},{"language":"en","tag":"Video","count":297},{"language":"ja","tag":"Comfy Router","count":289},{"language":"ko","tag":"Comfy Router","count":289},{"language":"zh","tag":"Comfy Router","count":289},{"language":"en","tag":"Models","count":273},{"language":"es","tag":"Video","count":272},{"language":"en","tag":"All Models","count":270},{"language":"es","tag":"Comfy Router","count":266},{"language":"ja","tag":"Models","count":259},{"language":"ko","tag":"Models","count":259},{"language":"zh","tag":"Models","count":259},{"language":"es","tag":"Models","count":227},{"language":"es","tag":"All Models","count":224},{"language":"ja","tag":"画像","count":211},{"language":"ko","tag":"이미지","count":211},{"language":"zh","tag":"图像","count":211},{"language":"ja","tag":"Video","count":197},{"language":"zh","tag":"Video","count":197},{"language":"ko","tag":"Video","count":180}],"searchSettings":{"maxResults":10}},"assistantConfig":{"enableStarterQuestions":false},"basePath":"","hasChatPermissions":false,"hasWebMcpPermissions":true,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-rbac-roles","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"custom.css","content":":root{--ink-900: #151417;--hub-shell-light: #f7f6f9;--accent-purple: #49378b;--accent-yellow: #f2ff59;--primary: 73 55 139 !important;--primary-light: 242 255 89 !important;--primary-dark: 242 255 89 !important}html.dark{--primary: 242 255 89 !important;--background-dark: 14 14 13 !important}html.dark body{background-color:#0e0e0d!important}html.dark #navbar,html.dark #navbar-transition{background-color:var(--ink-900)!important}html.dark #sidebar,html.dark #sidebar-content{background-color:rgb(var(--background-dark))!important}html.dark #navbar,html.dark #navbar-transition{box-shadow:none!important;filter:none!important;backdrop-filter:none!important;-webkit-backdrop-filter:none!important}html.dark #navbar-transition{border-bottom:1px solid rgba(255, 255, 255, 0.08)!important;opacity:1!important}html.dark img.nav-logo{filter:none}html.dark #sidebar-content a:not([aria-current=\"page\"]){color:rgba(255, 255, 255, 0.72)!important}html.dark #sidebar-content a:not([aria-current=\"page\"]) *{color:inherit!important}html.dark #sidebar-content a[aria-current=\"page\"]{color:var(--accent-yellow)!important;background-color:rgba(242, 255, 89, 0.10)!important}html.dark #sidebar-content a[aria-current=\"page\"] *{color:inherit!important}html.dark .nav-tabs-item[data-active],html.dark mobile-nav-tabs-item[data-active]{color:var(--accent-yellow)!important}html.dark #navbar a[href*=\"comfy.org/cloud\"] span.absolute{background-color:var(--accent-yellow)!important}html.dark #navbar a[href*=\"comfy.org/cloud\"] span.text-white{color:#151417!important}html:not(.dark) #background-color,html:not(.dark) #navbar,html:not(.dark) #navbar-transition,html:not(.dark) #sidebar,html:not(.dark) #sidebar-content{background-color:var(--hub-shell-light)!important}html:not(.dark) .nav-tabs-item[data-active],html:not(.dark) mobile-nav-tabs-item[data-active],html:not(.dark) nav-dropdown-item[data-active]{background-color:transparent!important;box-shadow:none!important;color:var(--accent-purple)!important;font-weight:600!important}html:not(.dark) .nav-tabs-item[data-active] *,html:not(.dark) mobile-nav-tabs-item[data-active] *,html:not(.dark) nav-dropdown-item[data-active] *{color:inherit!important}html:not(.dark) #sidebar-content li:has(>a[aria-current=\"page\"]){background-color:transparent!important;box-shadow:none!important}html:not(.dark) #sidebar-content a[aria-current=\"page\"]{color:var(--accent-purple)!important;background-color:rgba(73, 55, 139, 0.1)!important;font-weight:600!important}html:not(.dark) #sidebar-content a[aria-current=\"page\"] *{color:inherit!important}html:not(.dark) toc-item[data-active],html:not(.dark) toc-item[data-active-deepest],html:not(.dark) toc-item[data-active] a,html:not(.dark) toc-item[data-active-deepest] a{background-color:transparent!important;color:var(--accent-purple)!important;font-weight:600!important}html:not(.dark) #header :where(h1,h2,h3,h4,h5,h6),html:not(.dark) #content-area :where(h1,h2,h3,h4,h5,h6){color:#151417!important}html:not(.dark) #navbar a[href*=\"comfy.org/cloud\"]{background-color:transparent!important;color:var(--accent-purple)!important;border-radius:0.75rem;box-shadow:inset 0 0 0 1px var(--accent-purple)!important}html:not(.dark) #navbar a[href*=\"comfy.org/cloud\"] span.absolute{background-color:transparent!important}html:not(.dark) #navbar a[href*=\"comfy.org/cloud\"] *{color:inherit!important}#sidebar .sticky.top-0.pointer-events-none.bg-linear-to-b{background-image:none!important}@media (min-width: 1024px){#navbar.is-not-custom.is-not-center+div:has(>#sidebar),.max-w-8xl.mx-auto.relative,.max-w-8xl.mx-auto{max-width:none!important;width:100%!important;margin-left:0!important;margin-right:0!important;padding-left:0!important;padding-right:0!important}#navbar [class*=\"px-12\"],#navbar [class*=\"mx-4\"]{padding-left:0!important;padding-right:1.5rem!important;margin-left:0!important;margin-right:0!important}#navbar a.select-none,#navbar .nav-logo{margin-left:1rem!important}#navbar .nav-tabs{padding-left:2rem!important}#sidebar{left:0!important;margin-left:0!important;box-shadow:none!important}#sidebar-content{margin-left:0!important;padding-left:1rem!important;box-sizing:border-box!important}#navbar,#navbar-transition{left:0!important;right:0!important;width:100%!important;max-width:none!important}#content-area{margin-left:0!important;max-width:none!important;width:100%!important;padding-left:18rem!important}#sidebar.sticky~#content-container #content-area{padding-left:2rem!important;padding-right:2rem!important}#content-container,#body-content{max-width:none!important;width:100%!important}:root{--content-width: 100%;--sidebar-width: 18rem}}@media (min-width: 1024px){nav#sidebar,#sidebar[style]{left:0!important;right:auto!important;margin-left:0!important;transform:none!important}#navbar li.navbar-link:has(a[href*=\"comfy.org/download\"]){margin-right:0!important}#navbar a[href*=\"comfy.org/download\"]{box-sizing:border-box;border:1px solid rgb(255 255 255 / 0.22);border-radius:0.75rem;padding:7px 0.875rem!important;color:#fff!important;font-size:0.875rem;line-height:1.25rem}html:not(.dark) #navbar a[href*=\"comfy.org/download\"]{border-color:rgb(0 0 0 / 0.16);color:#151417!important}#navbar a[href*=\"comfy.org/download\"]:hover{background:rgb(255 255 255 / 0.06)}html:not(.dark) #navbar a[href*=\"comfy.org/download\"]:hover{background:rgb(0 0 0 / 0.04)}#navbar li.navbar-link:has(a[href*=\"comfy.org/download\"])+.whitespace-nowrap,#navbar .flex.space-x-6>.whitespace-nowrap.hidden{margin-left:0.5rem!important}}body:has(.router-api-reference-marker) #content,body:has(.router-api-reference-marker) #header{max-width:48rem}body:has(.router-api-reference-marker) #content-area table{width:100%;table-layout:auto}body:has(.router-api-reference-marker) #content-area th,body:has(.router-api-reference-marker) #content-area td{min-width:4rem!important;overflow-wrap:anywhere}body:has(.router-api-reference-marker) #content-area table:has(th:nth-child(6)) th:nth-child(3),body:has(.router-api-reference-marker) #content-area table:has(th:nth-child(6)) td:nth-child(3){min-width:5rem!important}@media (max-width: 1535px){body:has(.router-api-reference-marker) #content-side-layout{display:none!important}}body:has(.router-provider-coverage-marker) #content,body:has(.router-provider-coverage-marker) #header{max-width:64rem}body:has(.router-provider-coverage-marker) #content-area table{width:100%;table-layout:auto}body:has(.router-provider-coverage-marker) #content-area th,body:has(.router-provider-coverage-marker) #content-area td{min-width:5rem!important;overflow-wrap:anywhere}body:has(.router-provider-coverage-marker) #content-area th:first-child,body:has(.router-provider-coverage-marker) #content-area td:first-child{min-width:12rem!important}body:has(.router-quickstart-marker) [data-component-part=\"card-content\"] [data-component-part=\"code-block-root\"][style*=\"height:190px\"],body:has(.router-quickstart-marker) [data-component-part=\"card-content\"] [data-component-part=\"code-block-root\"][style*=\"height: 190px\"]{height:86px!important}"},{"filePath":"samples-cards.css","content":".sample-card-grid{display:grid;gap:20px;grid-template-columns:repeat(2, minmax(0, 1fr));margin-top:28px}.sample-card{min-width:0;perspective:1200px}.sample-card-inner{height:565px;position:relative;transform-style:preserve-3d;transition:transform 560ms cubic-bezier(.2, .75, .3, 1)}.sample-card[data-flipped=\"true\"] .sample-card-inner{transform:rotateY(180deg)}.sample-card-face{backface-visibility:hidden;background:#fff;border:1px solid #e5e1eb;border-radius:12px;box-sizing:border-box;height:100%;inset:0;overflow:hidden;position:absolute;width:100%}.sample-card-front{display:flex;flex-direction:column}.sample-card-back{opacity:0;pointer-events:none;transform:rotateY(180deg);visibility:hidden}.sample-card[data-flipped=\"true\"] .sample-card-front{pointer-events:none;transition:visibility 0s linear 280ms;visibility:hidden}.sample-card[data-flipped=\"true\"][data-active-path=\"api\"] .sample-card-back[data-sample-path=\"api\"],.sample-card[data-flipped=\"true\"][data-active-path=\"endpoint\"] .sample-card-back[data-sample-path=\"endpoint\"]{opacity:1;pointer-events:auto;visibility:visible}.sample-card-preview{border-bottom:1px solid #e5e1eb;cursor:pointer;display:block;width:100%}.sample-card-body{display:flex;flex:1;flex-direction:column;padding:20px}.sample-card-body h2,.sample-path-body h2{color:#49378b;font-size:18px;font-weight:700;letter-spacing:-.02em;margin:0 0 8px}.sample-path-body h2{text-align:center}.sample-card-body>p{color:#5f5966;font-size:14px;line-height:1.55;margin:0;min-height:66px}.sample-card-actions{display:flex;gap:8px;margin-top:20px}.sample-card-actions button{background:#fff;border:1px solid #cfc8de;border-radius:6px;color:#49378b;cursor:pointer;flex:1;font-size:13px;font-weight:700;padding:10px 12px}.sample-card-links{border-top:1px solid #eeeaf1;display:flex;gap:8px;margin-top:16px;padding-top:12px}.sample-card-links a{color:#625b6c;font-size:12px;font-weight:650;text-decoration:none}.sample-card-links span{color:#b3adba}.sample-path-body{padding:20px}.sample-path-body>button{background:transparent;border:0;color:#625b6c;cursor:pointer;font-size:12px;font-weight:700;padding:0}.sample-path-eyebrow{color:#857a9e;font-size:10px;font-weight:700;letter-spacing:.08em;margin:19px 0 5px;text-align:center;text-transform:uppercase}.sample-path-body>p:not(.sample-path-eyebrow){color:#5f5966;font-size:12px;line-height:1.5;margin:0 0 15px}.sample-path-steps{display:grid;gap:8px}.sample-path-step{align-items:center;background:#faf9fc;border:1px solid #e5e1ea;border-radius:8px;display:flex;gap:10px;padding:10px 11px}.sample-path-number{align-items:center;background:#49378b;border-radius:999px;color:#fff;display:inline-flex;flex:0 0 21px;font-size:11px;font-weight:700;height:21px;justify-content:center}.sample-path-copy{flex:1;min-width:0}.sample-path-copy strong{color:#312c36;display:block;font-size:12px}.sample-path-copy span{color:#706a75;display:block;font-size:11px;line-height:1.35;margin-top:2px}.sample-path-step a{color:#49378b;flex:0 0 auto;font-size:11px;font-weight:700;text-decoration:none}@media (max-width: 768px){.sample-card-grid{grid-template-columns:1fr}.sample-card-inner{height:clamp(620px, 180vw, 760px)}}"}],"page":{"pagePath":"es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately","slug":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately","pageMetadata":{"title":"Submit a partner model run to the queue and return immediately.","description":"Comfy Router's queued delivery mode. The request body is the same partner-native JSON input `POST /v2/models/{provider}/{model}` accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers `201` with a handle, and the caller collects the result later through the three reads below.\nChoose it over the synchronous route when the model's generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion.\nONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with `403` `not_enabled` and a `detail` that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key's workspace, the workspace is unknown rather than absent, and the answer is a retryable `503` `service_unavailable` instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the `403`, not a `402` `insufficient_credits`, whatever their balance, because adding credits would not change the answer.\nThe `201` carries `request_id` and the three URLs that address the rest of this request's lifetime, so a client never has to compose a queue URL itself. It also carries `queue_position` as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move.\n`request_id` IS NOT `X-Comfy-Request-Id` - not on a first submit and not on any other response. They are two identifiers with two jobs. `request_id` is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. `X-Comfy-Request-Id` is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel.\nREAD THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand.\nAN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. `RouterIdempotencyMiddleware` replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in `request_id` and, exactly as on every other response, this call's own id in the header. `Idempotent-Replayed: true` marks exactly those responses.\nSo `request_id` is authoritative on every response. Keep the body field.\nThis operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the synchronous route.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /v2/models/{provider}/{model}/requests","href":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately","autogeneratedByOpenApi":true},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx, jsxs: _jsxs} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n code: \"code\",\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsxs(_components.p, {\n children: [\"Comfy Router’s queued delivery mode. The request body is the same partner-native JSON input \", _jsx(_components.code, {\n children: \"POST /v2/models/{provider}/{model}\"\n }), \" accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers \", _jsx(_components.code, {\n children: \"201\"\n }), \" with a handle, and the caller collects the result later through the three reads below.\\nChoose it over the synchronous route when the model’s generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion.\\nONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with \", _jsx(_components.code, {\n children: \"403\"\n }), \" \", _jsx(_components.code, {\n children: \"not_enabled\"\n }), \" and a \", _jsx(_components.code, {\n children: \"detail\"\n }), \" that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key’s workspace, the workspace is unknown rather than absent, and the answer is a retryable \", _jsx(_components.code, {\n children: \"503\"\n }), \" \", _jsx(_components.code, {\n children: \"service_unavailable\"\n }), \" instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the \", _jsx(_components.code, {\n children: \"403\"\n }), \", not a \", _jsx(_components.code, {\n children: \"402\"\n }), \" \", _jsx(_components.code, {\n children: \"insufficient_credits\"\n }), \", whatever their balance, because adding credits would not change the answer.\\nThe \", _jsx(_components.code, {\n children: \"201\"\n }), \" carries \", _jsx(_components.code, {\n children: \"request_id\"\n }), \" and the three URLs that address the rest of this request’s lifetime, so a client never has to compose a queue URL itself. It also carries \", _jsx(_components.code, {\n children: \"queue_position\"\n }), \" as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move.\\n\", _jsx(_components.code, {\n children: \"request_id\"\n }), \" IS NOT \", _jsx(_components.code, {\n children: \"X-Comfy-Request-Id\"\n }), \" - not on a first submit and not on any other response. They are two identifiers with two jobs. \", _jsx(_components.code, {\n children: \"request_id\"\n }), \" is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. \", _jsx(_components.code, {\n children: \"X-Comfy-Request-Id\"\n }), \" is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel.\\nREAD THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand.\\nAN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. \", _jsx(_components.code, {\n children: \"RouterIdempotencyMiddleware\"\n }), \" replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in \", _jsx(_components.code, {\n children: \"request_id\"\n }), \" and, exactly as on every other response, this call’s own id in the header. \", _jsx(_components.code, {\n children: \"Idempotent-Replayed: true\"\n }), \" marks exactly those responses.\\nSo \", _jsx(_components.code, {\n children: \"request_id\"\n }), \" is authoritative on every response. Keep the body field.\\nThis operation is deliberately tagged \", _jsx(_components.code, {\n children: \"Comfy Router\"\n }), \" and NOT \", _jsx(_components.code, {\n children: \"API Nodes\"\n }), \", for the reason given on the synchronous route.\"]\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){return _jsx(_Fragment,{})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Submit a partner model run to the queue and return immediately.","description":"Comfy Router's queued delivery mode. The request body is the same partner-native JSON input `POST /v2/models/{provider}/{model}` accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers `201` with a handle, and the caller collects the result later through the three reads below.\nChoose it over the synchronous route when the model's generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion.\nONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with `403` `not_enabled` and a `detail` that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key's workspace, the workspace is unknown rather than absent, and the answer is a retryable `503` `service_unavailable` instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the `403`, not a `402` `insufficient_credits`, whatever their balance, because adding credits would not change the answer.\nThe `201` carries `request_id` and the three URLs that address the rest of this request's lifetime, so a client never has to compose a queue URL itself. It also carries `queue_position` as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move.\n`request_id` IS NOT `X-Comfy-Request-Id` - not on a first submit and not on any other response. They are two identifiers with two jobs. `request_id` is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. `X-Comfy-Request-Id` is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel.\nREAD THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand.\nAN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. `RouterIdempotencyMiddleware` replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in `request_id` and, exactly as on every other response, this call's own id in the header. `Idempotent-Replayed: true` marks exactly those responses.\nSo `request_id` is authoritative on every response. Keep the body field.\nThis operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the synchronous route.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /v2/models/{provider}/{model}/requests","href":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately","autogeneratedByOpenApi":true}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[],"codeExamples":{}},"jsLinks":["/_mintlify/custom/dripart/9d6bf2b2531e904c/giscus-comments.js","/_mintlify/custom/dripart/bd0341946f3c75a6/syft.js","/_mintlify/custom/dripart/042392a43b5e681e/samples-cards.js"],"openApiReferenceData":{"operation":{"description":"Comfy Router's queued delivery mode. The request body is the same partner-native JSON input `POST /v2/models/{provider}/{model}` accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers `201` with a handle, and the caller collects the result later through the three reads below.\nChoose it over the synchronous route when the model's generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion.\nONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with `403` `not_enabled` and a `detail` that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key's workspace, the workspace is unknown rather than absent, and the answer is a retryable `503` `service_unavailable` instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the `403`, not a `402` `insufficient_credits`, whatever their balance, because adding credits would not change the answer.\nThe `201` carries `request_id` and the three URLs that address the rest of this request's lifetime, so a client never has to compose a queue URL itself. It also carries `queue_position` as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move.\n`request_id` IS NOT `X-Comfy-Request-Id` - not on a first submit and not on any other response. They are two identifiers with two jobs. `request_id` is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. `X-Comfy-Request-Id` is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel.\nREAD THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand.\nAN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. `RouterIdempotencyMiddleware` replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in `request_id` and, exactly as on every other response, this call's own id in the header. `Idempotent-Replayed: true` marks exactly those responses.\nSo `request_id` is authoritative on every response. Keep the body field.\nThis operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the synchronous route.","operationId":"SubmitRouterModelRequest","parameters":["765bdee0-dfa7-4ffc-82b1-be0de060375f","a5c77c56-16f8-46aa-a5e8-7cca45e4c6a0","cf305310-0605-49f3-b3da-cdf6edac0f9f","ec10517c-6f3e-41ca-82bd-a7088a971b19","5e87a2c2-2c85-4302-bfae-47a5f6ca9119","9eb586c4-fbec-4924-802d-1e561058d443"],"requestBody":"fc05d750-2947-43c1-a10e-bc35954ac6fe","responses":{"201":"c56e1afd-f805-4b06-a697-a7eb12216ff9","400":"0e48f5ac-3f62-41ec-8c98-c2acd9b2bb3d","401":"7fc087b1-e2bd-42d9-a1eb-a50c9396413b","402":"338c7010-a706-4c6f-a594-2e98b7c99ebc","403":"6f93f168-c268-43a7-9e7c-4d685bedd453","404":"e2f18ce0-b497-410e-9d1d-142270762628","409":"353347b3-8126-4f9e-83b7-825d9a2e8ffa","413":"8504d484-d48e-4d6c-a6e8-8c0ebacdebe9","422":"aa5160a7-f935-4684-9be4-c529d5145b01","429":"742d7b37-e4a3-4f10-8aef-3297eddcf791","503":"94ec999d-fafd-4dd7-8ac8-54eb1e24cf18","504":"a6b5a441-5a59-4f2b-8002-9a6ea08fd9a3"},"security":["84ebca82-60bf-47a3-ac72-11fd5e2f5b57","2d7e774d-cee7-4b74-8eec-ecf2ba2bcc38"],"summary":"Submit a partner model run to the queue and return immediately.","tags":["Comfy Router"],"method":"post","type":"path","servers":["0579de4e-d11b-4f34-98ad-304ea266ca73"],"path":"/v2/models/{provider}/{model}/requests","baseUrlOptions":["https://api.comfy.org"],"title":"Submit a partner model run to the queue and return immediately.","codeSamples":[],"requiredOnlyExamples":false,"requestExampleType":"generated","responseExampleType":"generated"},"dependencies":{"parameters":{"765bdee0-dfa7-4ffc-82b1-be0de060375f":{"description":"Lowercase provider segment of the canonical `{provider}/{model}` model ID - the partner whose model is being run.\nThe 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":{"description":"Lowercase provider segment of the canonical `{provider}/{model}` model ID - the partner whose model is being run.\nThe 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.\nLowercase `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.\nThe `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","isRequired":true,"uniqueKey":"","typeLabel":"string"}},"a5c77c56-16f8-46aa-a5e8-7cca45e4c6a0":{"description":"Lowercase model segment of the canonical `{provider}/{model}` model ID - the model to run within that provider.\nAs 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":{"description":"Lowercase model segment of the canonical `{provider}/{model}` model ID - the model to run within that provider.\nAs with `provider`, the schema is the shared `RouterModelSegment` component and its `pattern` documents the contract rather than enforcing it - see `RouterProvider`.\nLowercase `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`.\nAs 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","isRequired":true,"uniqueKey":"","typeLabel":"string"}},"cf305310-0605-49f3-b3da-cdf6edac0f9f":{"description":"Selects an alternate provider to serve this model, instead of its current default. Omitting it runs native dispatch on the model's own default provider; `default`, `comfy`, and `comfyui` are aliases for that same native behavior and name no override, because Comfy Router is never itself a serving backend. The alternate providers Router can retarget a model onto are `fal`, `wavespeed`, `runware`, and `higgsfield`; which of them a given model supports is reported by `GET /v2/models/{provider}/{model}`. When an alternate provider is selected, the native request body is translated into that provider's real schema unless `strict_mode=true`; see `strict_mode` and `fallback_provider`. Its refusals are checked in a fixed order, and an earlier one answers whether or not a later one would. A value that is not a registered provider at all is refused `400` with `error_type: invalid_input`. Then a workspace configured for direct provider calls only refuses any non-default `model_provider` with `403` and `error_type: forbidden`, and Router never runs its `fallback_provider` retry for it; a resubmit carrying the `Idempotency-Key` of a request already accepted is answered as a replay of that request rather than refused. Then a request whose body selects a multipart operation (an edit - for example gpt-image's `image` field) is refused `409`, also with `error_type: invalid_input`, whose `detail` begins \"this request's `image` selects the edit operation\" - but only when the named provider's translator for this model cannot itself serve that operation; a leg whose translator does carry the media is not refused and proceeds normally, so this is a per-leg refusal rather than a blanket one. Then a request that has already resolved a bring-your-own-key credential for the provider in the path is refused `409`, also with `error_type: invalid_input`, whose `detail` begins `this request resolved a BYOK credential` - see the `409` on this route, where that `detail` is the only thing separating this case, and the multipart case above, from the `Idempotency-Key` one. That BYOK check runs whether or not the named provider has a leg for this model. Then the named provider's own gate refuses `403` with `error_type: not_enabled` when that provider is not turned on for you, and `503` with `error_type: service_unavailable` when the gate cannot be evaluated - a flag-evaluation failure, or a missing or nil gate entry. Only past all of those is a real provider that does not serve this model refused `400` with `error_type: invalid_input`, the same answer as a value that is not a registered provider at all - both mean the `model_provider` value cannot serve this model, and that `400`'s `detail` is human-readable and not a contract, so read `GET /v2/models/{provider}/{model}` to learn which providers a model does support rather than parsing it. Then, on a Seedance 2.x model, a body carrying an `asset://` reference in any `content[]` image, video or audio url is refused `400` with `error_type: invalid_input`: those ids are BytePlus virtual-library assets no other provider can serve, so send the media itself as an http(s) URL or a `data:` URI to use `model_provider`. Past those `400`s, this workspace's partner-provider policy for the named vendor is evaluated too and can refuse `403` or `503` of its own. Neither this parameter nor `fallback_provider` is available on a BYOK request, and the two are unavailable in different ways: the credential was resolved for the provider named in the path while every alternate leg dispatches on Comfy's own key, so an explicit `model_provider` is refused with that `409`, and `fallback_provider` is inert rather than refused - no retry against an alternate provider is attempted and the first attempt's own failure is what the caller receives. The same split applies to a multipart body: only an explicit `model_provider` reaches the `409` above, and only for a leg whose translator cannot serve the operation, while the automatic on-failure retry `fallback_provider` controls is simply skipped (inert, not refused) for any multipart body. Router defines no `provider_not_available` or `validation_error` `error_type`: these conditions fold onto `invalid_input`. The `error_type` set can still grow, so treat any value you do not recognize as `internal_error` rather than switching exhaustively.","in":"query","name":"model_provider","schema":{"type":"string","description":"Selects an alternate provider to serve this model, instead of its current default. Omitting it runs native dispatch on the model's own default provider; `default`, `comfy`, and `comfyui` are aliases for that same native behavior and name no override, because Comfy Router is never itself a serving backend. The alternate providers Router can retarget a model onto are `fal`, `wavespeed`, `runware`, and `higgsfield`; which of them a given model supports is reported by `GET /v2/models/{provider}/{model}`. When an alternate provider is selected, the native request body is translated into that provider's real schema unless `strict_mode=true`; see `strict_mode` and `fallback_provider`. Its refusals are checked in a fixed order, and an earlier one answers whether or not a later one would. A value that is not a registered provider at all is refused `400` with `error_type: invalid_input`. Then a workspace configured for direct provider calls only refuses any non-default `model_provider` with `403` and `error_type: forbidden`, and Router never runs its `fallback_provider` retry for it; a resubmit carrying the `Idempotency-Key` of a request already accepted is answered as a replay of that request rather than refused. Then a request whose body selects a multipart operation (an edit - for example gpt-image's `image` field) is refused `409`, also with `error_type: invalid_input`, whose `detail` begins \"this request's `image` selects the edit operation\" - but only when the named provider's translator for this model cannot itself serve that operation; a leg whose translator does carry the media is not refused and proceeds normally, so this is a per-leg refusal rather than a blanket one. Then a request that has already resolved a bring-your-own-key credential for the provider in the path is refused `409`, also with `error_type: invalid_input`, whose `detail` begins `this request resolved a BYOK credential` - see the `409` on this route, where that `detail` is the only thing separating this case, and the multipart case above, from the `Idempotency-Key` one. That BYOK check runs whether or not the named provider has a leg for this model. Then the named provider's own gate refuses `403` with `error_type: not_enabled` when that provider is not turned on for you, and `503` with `error_type: service_unavailable` when the gate cannot be evaluated - a flag-evaluation failure, or a missing or nil gate entry. Only past all of those is a real provider that does not serve this model refused `400` with `error_type: invalid_input`, the same answer as a value that is not a registered provider at all - both mean the `model_provider` value cannot serve this model, and that `400`'s `detail` is human-readable and not a contract, so read `GET /v2/models/{provider}/{model}` to learn which providers a model does support rather than parsing it. Then, on a Seedance 2.x model, a body carrying an `asset://` reference in any `content[]` image, video or audio url is refused `400` with `error_type: invalid_input`: those ids are BytePlus virtual-library assets no other provider can serve, so send the media itself as an http(s) URL or a `data:` URI to use `model_provider`. Past those `400`s, this workspace's partner-provider policy for the named vendor is evaluated too and can refuse `403` or `503` of its own. Neither this parameter nor `fallback_provider` is available on a BYOK request, and the two are unavailable in different ways: the credential was resolved for the provider named in the path while every alternate leg dispatches on Comfy's own key, so an explicit `model_provider` is refused with that `409`, and `fallback_provider` is inert rather than refused - no retry against an alternate provider is attempted and the first attempt's own failure is what the caller receives. The same split applies to a multipart body: only an explicit `model_provider` reaches the `409` above, and only for a leg whose translator cannot serve the operation, while the automatic on-failure retry `fallback_provider` controls is simply skipped (inert, not refused) for any multipart body. Router defines no `provider_not_available` or `validation_error` `error_type`: these conditions fold onto `invalid_input`. The `error_type` set can still grow, so treat any value you do not recognize as `internal_error` rather than switching exhaustively.","uniqueKey":"","typeLabel":"string"}},"ec10517c-6f3e-41ca-82bd-a7088a971b19":{"description":"Only meaningful together with `model_provider`. `false` (the default): the request body must be this model's own native contract, translated to the alternate provider's real schema - any native field that cannot be expressed exactly is dropped and disclosed via the response's `X-Comfy-Router-Dropped-Params` header, never silently. `true`: the body must already be the alternate provider's own real schema, passed through unmodified in both directions - no translation, so `X-Comfy-Router-Dropped-Params` is never sent, and provider fallback is disabled for the call because a body shaped for one provider cannot be replayed against another (see `fallback_provider`).","in":"query","name":"strict_mode","schema":{"default":false,"type":"boolean","description":"Only meaningful together with `model_provider`. `false` (the default): the request body must be this model's own native contract, translated to the alternate provider's real schema - any native field that cannot be expressed exactly is dropped and disclosed via the response's `X-Comfy-Router-Dropped-Params` header, never silently. `true`: the body must already be the alternate provider's own real schema, passed through unmodified in both directions - no translation, so `X-Comfy-Router-Dropped-Params` is never sent, and provider fallback is disabled for the call because a body shaped for one provider cannot be replayed against another (see `fallback_provider`).","uniqueKey":"","typeLabel":"boolean"}},"5e87a2c2-2c85-4302-bfae-47a5f6ca9119":{"description":"Caller-generated key that makes retrying one logical call safe. A call that reached the caller with an answer is recorded against its key for 24 hours, and a retry carrying the same key is answered from that record instead of dispatching - and charging - the provider a second time, marked `Idempotent-Replayed: true`. Keys are scoped to the workspace your credential carries, or to your user when it carries none - so the keyspace is shared by every member of a workspace rather than private to one caller. Make a key unique across the whole workspace, not just within your own client: a second member who reuses a key string is answered from the first member's record, or refused `409` if the request differs. Because the scope follows the credential and not the person, a credential that carries no workspace at all scopes to your user id instead - so retrying one logical call under a different credential can land in a different namespace, where it is dispatched and charged again. Retry with the credential you started with. A keyed request with no authenticated caller is refused `401`. The guarantee is a billing one: a key is charged at most once. It is not a promise that a key is dispatched at most once, and it does not make a lost call resumable. Some answers are recorded but not replayable for the full 24 hours, and the billing guarantee is the half that always holds: the key stays consumed - the retry never re-runs and never re-charges - but it is answered `409 invalid_input` instead of being served the original body. That happens whenever Comfy does not hold a copy of the response it can still stand behind; a response past the replay size cap and a result addressed by an asset URL Comfy does not host are the two you are most likely to meet. The second is the one worth planning for, because it looks like an ordinary success: which models answer with a Comfy-hosted asset link, how long one stays valid, and what a result carries when an individual asset could not be copied are stated in one place, under Result assets in the API reference, and this paragraph does not restate them. On a model that returns its result on the original call, an answer still holding a partner's own asset link is replayed for a few minutes - which is where a dropped connection puts an SDK's automatic same-key re-send, and while the partner's link is certainly still alive - and refused after that rather than replayed dead. So a prompt retry of a partially re-hosted result behaves exactly like any other replay, and only a later one meets the `409`. That short window is deliberately not offered on a model that submits and is polled, because there the partner may have minted the URL long before your call collected it and its remaining life is unknowable - and those models do not need it: a call cut off mid-generation keeps its key holding the generation, so the same-key retry collects the original result rather than a recorded copy of it. A response past the size cap has no window either and is refused from the start. The action on any of these `409 invalid_input` refusals is the same: use a new key. Only an answer a provider actually produced is recorded, though. A refusal Router raises on its own before dispatching anything - not enabled for you yet (`403`), unknown model (`404`), not entitled to the model (`403`), a body the model's schema rejects or that names a different model than the path (`422`), a malformed request (`400 invalid_input`) - dispatched nothing and charged nothing, so it releases the key: re-send the same key once you are on the rollout ramp or have corrected the request and it runs for real, rather than replaying the refusal or colliding with it as a `409`. That turns on whether a provider was reached, never on the status, so a `400 content_policy_violation` - the partner's own answer to a call that ran, which some models meter - is recorded and replayed like any other answer. Releasing a refusal that dispatched nothing frees nothing chargeable, so it does not weaken the at-most-once billing guarantee above.\nGenerate a fresh key per logical call and reuse it across that call's retry attempts; a UUID is the intended shape. Persist the key BEFORE sending the request - a key held only in the memory of the process that crashed cannot be resent, and the retry that would have collected the result becomes a fresh, separately charged run.\nA retry that arrives while the original is still in flight is answered `409` with `Retry-After`: wait and re-send the same key. The same key presented with a DIFFERENT request is a `409` too, rather than a silent overwrite - and \"different\" is judged on the whole request, not the body alone: the method, the path and query string, and the body must all match the original. One exception: a signed Comfy asset URL in the body (a Comfy-issued `storage.googleapis.com` URL, such as the `download_url` an input upload returns) is compared without its signature - the signing query parameters and any fragment - so a retry that re-signed the same input object still matches the original and collects its answer. Any other change to that URL, such as a different object or path, is a different request. A call whose recorded answer is an error is matched on the exact body instead, because the signature may be what failed: a re-signed retry of it is the different-request `409`, and the remedy is a new key.\nTHE KEY IS NEVER RETURNED IN ANY RESPONSE - not on the `200`, not on the `504`, not in any error body. It is yours to mint and yours to keep: a caller who did not keep it has no way to collect a generation they were billed for. A `deadline_exceeded` `504` on a generation the provider is still running answers a same-key retry with `504` and `Retry-After` again until it finishes, then with the result.\nOmitting the header is allowed and leaves the call unguarded rather than refused: there is nothing to check, so every retry is a fresh dispatch and a fresh charge.","in":"header","name":"Idempotency-Key","schema":{"example":"6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21","maxLength":255,"minLength":1,"type":"string","description":"Caller-generated key that makes retrying one logical call safe. A call that reached the caller with an answer is recorded against its key for 24 hours, and a retry carrying the same key is answered from that record instead of dispatching - and charging - the provider a second time, marked `Idempotent-Replayed: true`. Keys are scoped to the workspace your credential carries, or to your user when it carries none - so the keyspace is shared by every member of a workspace rather than private to one caller. Make a key unique across the whole workspace, not just within your own client: a second member who reuses a key string is answered from the first member's record, or refused `409` if the request differs. Because the scope follows the credential and not the person, a credential that carries no workspace at all scopes to your user id instead - so retrying one logical call under a different credential can land in a different namespace, where it is dispatched and charged again. Retry with the credential you started with. A keyed request with no authenticated caller is refused `401`. The guarantee is a billing one: a key is charged at most once. It is not a promise that a key is dispatched at most once, and it does not make a lost call resumable. Some answers are recorded but not replayable for the full 24 hours, and the billing guarantee is the half that always holds: the key stays consumed - the retry never re-runs and never re-charges - but it is answered `409 invalid_input` instead of being served the original body. That happens whenever Comfy does not hold a copy of the response it can still stand behind; a response past the replay size cap and a result addressed by an asset URL Comfy does not host are the two you are most likely to meet. The second is the one worth planning for, because it looks like an ordinary success: which models answer with a Comfy-hosted asset link, how long one stays valid, and what a result carries when an individual asset could not be copied are stated in one place, under Result assets in the API reference, and this paragraph does not restate them. On a model that returns its result on the original call, an answer still holding a partner's own asset link is replayed for a few minutes - which is where a dropped connection puts an SDK's automatic same-key re-send, and while the partner's link is certainly still alive - and refused after that rather than replayed dead. So a prompt retry of a partially re-hosted result behaves exactly like any other replay, and only a later one meets the `409`. That short window is deliberately not offered on a model that submits and is polled, because there the partner may have minted the URL long before your call collected it and its remaining life is unknowable - and those models do not need it: a call cut off mid-generation keeps its key holding the generation, so the same-key retry collects the original result rather than a recorded copy of it. A response past the size cap has no window either and is refused from the start. The action on any of these `409 invalid_input` refusals is the same: use a new key. Only an answer a provider actually produced is recorded, though. A refusal Router raises on its own before dispatching anything - not enabled for you yet (`403`), unknown model (`404`), not entitled to the model (`403`), a body the model's schema rejects or that names a different model than the path (`422`), a malformed request (`400 invalid_input`) - dispatched nothing and charged nothing, so it releases the key: re-send the same key once you are on the rollout ramp or have corrected the request and it runs for real, rather than replaying the refusal or colliding with it as a `409`. That turns on whether a provider was reached, never on the status, so a `400 content_policy_violation` - the partner's own answer to a call that ran, which some models meter - is recorded and replayed like any other answer. Releasing a refusal that dispatched nothing frees nothing chargeable, so it does not weaken the at-most-once billing guarantee above.\nGenerate a fresh key per logical call and reuse it across that call's retry attempts; a UUID is the intended shape. Persist the key BEFORE sending the request - a key held only in the memory of the process that crashed cannot be resent, and the retry that would have collected the result becomes a fresh, separately charged run.\nA retry that arrives while the original is still in flight is answered `409` with `Retry-After`: wait and re-send the same key. The same key presented with a DIFFERENT request is a `409` too, rather than a silent overwrite - and \"different\" is judged on the whole request, not the body alone: the method, the path and query string, and the body must all match the original. One exception: a signed Comfy asset URL in the body (a Comfy-issued `storage.googleapis.com` URL, such as the `download_url` an input upload returns) is compared without its signature - the signing query parameters and any fragment - so a retry that re-signed the same input object still matches the original and collects its answer. Any other change to that URL, such as a different object or path, is a different request. A call whose recorded answer is an error is matched on the exact body instead, because the signature may be what failed: a re-signed retry of it is the different-request `409`, and the remedy is a new key.\nTHE KEY IS NEVER RETURNED IN ANY RESPONSE - not on the `200`, not on the `504`, not in any error body. It is yours to mint and yours to keep: a caller who did not keep it has no way to collect a generation they were billed for. A `deadline_exceeded` `504` on a generation the provider is still running answers a same-key retry with `504` and `Retry-After` again until it finishes, then with the result.\nOmitting the header is allowed and leaves the call unguarded rather than refused: there is nothing to check, so every retry is a fresh dispatch and a fresh charge.","uniqueKey":"","typeLabel":"string"}},"9eb586c4-fbec-4924-802d-1e561058d443":{"description":"Opt in to refusing a top-level request-body field this model's input schema does not declare, rather than accepting it; nested fields are not checked, and nothing is refused for a model whose schema allows undeclared fields or has no authored schema, or on a queued submit sent with `strict_mode=true`.\nOmitted, or `false` (the default): unchanged behaviour. Most model input schemas here are open, so a field Comfy has not authored is accepted and forwarded, which is what lets a field a partner already supports reach it before Comfy documents it. The cost is that a misspelled field name, or a nested field written at the top level, is accepted too - and then dropped on the way to the provider, so the generation runs, and is billed, under the defaults that field was meant to change.\n`true`: a top-level field the published input schema does not declare is answered 422 with an `extra_forbidden` entry naming it, before any provider call is made and before anything is billed. Only the top level is checked, and only names that match no declared property even case-insensitively - a field whose spelling differs from a declared one only by case is already refused (`property_name_case`) regardless of this parameter. A model with no authored input schema declares no properties, so it refuses nothing here.\nWhat it refuses is a FLOOR, not a ceiling, in two stated cases. A model whose published schema sets `additionalProperties` itself is left exactly as it is: where that is `false` the schema already refuses undeclared fields with or without this parameter, and where it is `true` - as on the Anthropic and Gemini Interactions models, whose schemas promise fields such as `tools`, `temperature` and `safety_settings` pass through unchanged - the schema has declared that an undeclared field is allowed, and this parameter does not overrule it. And where a schema is a union (`oneOf`/`anyOf`), a field belonging to any branch is accepted, not only to the branch the body matched.\nThe parameter is per-request on purpose: closing every schema instead would start refusing bodies that succeed today, with no way for an existing caller to keep the behaviour they built against. It is accepted on both the synchronous `POST` and the queued `POST .../requests` submit, which validate the same body against the same schema. The one exception is `strict_mode=true` on the queued submit: that body is already in the alternate provider's own shape, so the queue does not validate it against this model's schema, and this parameter is accepted but has no effect there.","in":"query","name":"reject_unknown_fields","schema":{"default":false,"type":"boolean","description":"Opt in to refusing a top-level request-body field this model's input schema does not declare, rather than accepting it; nested fields are not checked, and nothing is refused for a model whose schema allows undeclared fields or has no authored schema, or on a queued submit sent with `strict_mode=true`.\nOmitted, or `false` (the default): unchanged behaviour. Most model input schemas here are open, so a field Comfy has not authored is accepted and forwarded, which is what lets a field a partner already supports reach it before Comfy documents it. The cost is that a misspelled field name, or a nested field written at the top level, is accepted too - and then dropped on the way to the provider, so the generation runs, and is billed, under the defaults that field was meant to change.\n`true`: a top-level field the published input schema does not declare is answered 422 with an `extra_forbidden` entry naming it, before any provider call is made and before anything is billed. Only the top level is checked, and only names that match no declared property even case-insensitively - a field whose spelling differs from a declared one only by case is already refused (`property_name_case`) regardless of this parameter. A model with no authored input schema declares no properties, so it refuses nothing here.\nWhat it refuses is a FLOOR, not a ceiling, in two stated cases. A model whose published schema sets `additionalProperties` itself is left exactly as it is: where that is `false` the schema already refuses undeclared fields with or without this parameter, and where it is `true` - as on the Anthropic and Gemini Interactions models, whose schemas promise fields such as `tools`, `temperature` and `safety_settings` pass through unchanged - the schema has declared that an undeclared field is allowed, and this parameter does not overrule it. And where a schema is a union (`oneOf`/`anyOf`), a field belonging to any branch is accepted, not only to the branch the body matched.\nThe parameter is per-request on purpose: closing every schema instead would start refusing bodies that succeed today, with no way for an existing caller to keep the behaviour they built against. It is accepted on both the synchronous `POST` and the queued `POST .../requests` submit, which validate the same body against the same schema. The one exception is `strict_mode=true` on the queued submit: that body is already in the alternate provider's own shape, so the queue does not validate it against this model's schema, and this parameter is accepted but has no effect there.","uniqueKey":"","typeLabel":"boolean"}}},"requestBody":{"content":{"application/json":{"schema":{"additionalProperties":true,"description":"A partner model's native JSON input document, forwarded to the provider 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.\nDeclaring 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","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{}}},"exampleType":"generated"}},"description":"The partner model's native JSON input, identical to the body the synchronous route accepts for this model, and it selects the operation and is metered the same way: the body's own fields choose which operation Router runs on a model that supports more than one (an input image switches an editable image model to image-to-image, which may be metered on its own rate; a Seedance first-frame image selects image-to-video and a reference image or clip selects reference-to-video). On native dispatch a Seedance request bills by the media kinds in `content`, not the roles: any video input bills as video-to-video, otherwise any image bills as image-to-video, otherwise text-to-video, except that a final render from a `draft_task` item bills as the operation of the Draft it renders, with some models charging the same rate for text-to-video and image-to-video; with `model_provider` selecting an alternate provider, each conditioned operation may be metered on that provider's own rate. The same provider-selection contract applies at dispatch: without `model_provider`, or with `model_provider` and `strict_mode=false` (the default), the body is the model's native document and is validated against the model's own input schema before the run is admitted, so a body the model would reject is a `422` here rather than a queued request that fails minutes later - a non-strict alternate-provider body is additionally translated into that provider's real schema at dispatch. With `strict_mode=true` the body must already be the alternate provider's own schema and is forwarded unchanged: native-schema validation is skipped, exactly as on the synchronous route (see `model_provider` and `strict_mode`).","required":true,"exampleType":"generated"},"responses":{"c56e1afd-f805-4b06-a697-a7eb12216ff9":{"content":{"application/json":{"schema":{"type":"object","description":"The handle returned when a run is admitted to the queue: the request's identity and state, composed with the three URLs that address the rest of its lifetime.\nIt is `allOf: [RouterQueueUrls, ...]` so that the URL half IS the same shape the status read returns rather than a copy of it, and a generated client can hand a submission to anything that accepts a queue handle.","required":["request_id","status","status_url","response_url","cancel_url"],"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","isRequired":true,"uniqueKey":"cancel_url","typeLabel":"string\u003curi>"},"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","isRequired":true,"uniqueKey":"response_url","typeLabel":"string\u003curi>"},"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","isRequired":true,"uniqueKey":"status_url","typeLabel":"string\u003curi>"},"request_id":{"description":"Identifier of one queued Router request - the handle a caller polls, cancels and collects a result by.\nIT 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`.\nREAD 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.\nIt 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","isRequired":true,"uniqueKey":"request_id","typeLabel":"string\u003cuuid>"},"status":{"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.\nTHERE 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","isRequired":true,"uniqueKey":"status","typeLabel":"enum\u003cstring>"},"estimate":{"type":"object","description":"The pre-flight cost estimate for this request, present on a fresh admission when the cost estimate is enabled for the caller. It quotes the provider leg this request was pinned to (the queue has no fallback loop, so `fallback_provider` never changes the leg that runs). When no figure can be given it is still present, with `source: unknown`; the submission never fails because of the estimate. Omitted on an idempotent replay (`Idempotent-Replayed: true`), because the original quote is not stored. It is an estimate, not the final charge, and not a price lock: the run is charged at the rates in force when it is rated, and the settled charge in your workspace's usage and billing records is final.","required":["source","currency","provider","model","pricing_as_of"],"properties":{"currency":{"description":"Always `USD`.","type":"string","isRequired":true,"uniqueKey":"estimate.currency","typeLabel":"string"},"model":{"description":"A canonical Comfy Router model ID, `{provider}/{model}` - exactly the value that addresses the model on `POST /v2/models/{provider}/{model}`, so a caller can interpolate it into that path without re-deriving it from anything. Its `pattern` is `RouterProviderSegment` and `RouterModelSegment` joined by a single `/`, and `maxLength` is their sum plus that separator.\nTestRouterCatalogIdsMatchInvocationRoute probes all three patterns behaviourally against one corpus, so loosening or tightening any of them alone fails CI rather than silently letting the catalog advertise an ID the invocation route would reject. The optional `variant` third segment is deliberately absent: how a variant is addressed is not settled by the invocation contract, so the catalog must not list an ID that route is not yet defined to accept. The PUBLIC lead paragraphs of `RouterProviderSegment`, `RouterModelSegment` and the `RouterProvider` / `RouterModel` path parameters therefore spell the canonical ID `{provider}/{model}` with no variant suffix, and must keep doing so while the variant form is unsettled: those leads are the only text the SDK projection publishes, so a three-segment form there vendors an ID shape every published `pattern` rejects and no path template accepts AS A MODEL ID. (`/v2/models/{provider}/{model}/openapi.json` does match a third segment, but only when it is that one literal - which is exactly the collision `getRouterModelInputSchema` reserves against, not an ID the route accepts.) The reservation is argued in the rationale paragraphs, which are never projected, and that is where it belongs. ONE bound does not survive the composition: `maxLength` here is the TOTAL, so a 193-character ID made of a 100-character provider and a 92-character model satisfies this schema while its provider segment exceeds `RouterProviderSegment`'s own 64. A single `pattern` cannot express a per-segment length bound - the structural alphabet and a character count are not jointly expressible without lookahead, which this repo's Go-side pattern probes cannot compile. The per-segment bounds are therefore carried by the sibling `provider` and `model` fields of `RouterModelListEntry`, which reference the bounded segment schemas directly, so no CONFORMING entry can carry such an `id`. TestRouterCatalogIdsMatchInvocationRoute pins that this residual is length-only: over-length segments are in its corpus, and any divergence that is not purely a per-segment length overrun fails.","example":"bfl/flux-2-pro","maxLength":193,"pattern":"^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$","type":"string","isRequired":true,"uniqueKey":"estimate.model","typeLabel":"string"},"pricing_as_of":{"description":"When the rate card this quote was priced from was fetched. The estimate is not a price lock: the run is charged at the rates in force when it is rated, not at this instant. An `estimated` quote taken from the vendor's published price list (a model billed the cost the vendor reports) carries the date that list was read instead. A quote whose billed quantity is predicted from a published vendor schedule and then priced from the rate card carries the older of that schedule's read date and the card's fetch time, for an `exact` answer and an `estimated` one alike (a model billed the credits its vendor reports is the `estimated` case). Whenever no rate card was read (`reason: pricing_unavailable`, `byok`, or a `not_quotable` call that cannot be priced from any card), this is when the quote was answered.","format":"date-time","type":"string","isRequired":true,"uniqueKey":"estimate.pricing_as_of","typeLabel":"string\u003cdate-time>"},"provider":{"description":"The provider leg this quote is for. On a queued submit it is the leg that will run.","type":"string","isRequired":true,"uniqueKey":"estimate.provider","typeLabel":"string"},"source":{"description":"`exact` | `estimated` | `free` | `unknown`; read any other value as `unknown`. `exact` and `estimated` both label an estimate; neither is the final charge. `free` means the rate card prices this request at zero, so `amount` is \"0\". `unknown` carries no figure.","type":"string","isRequired":true,"uniqueKey":"estimate.source","typeLabel":"string"},"amount":{"description":"The estimated cost in dollars as a decimal string, e.g. \"0.04\", with up to 8 decimals and no exponent. A chargeable sub-cent price is never rendered as \"0\"; only a `free` quote reads \"0\". Present only when `source` is `exact` or `free`. Not the final charge: the settled charge in your workspace's usage and billing records is.","type":"string","uniqueKey":"estimate.amount","typeLabel":"string"},"amount_cents":{"description":"The same figure in US cents, unrounded, for arithmetic. Present with `amount`.","format":"double","type":"number","uniqueKey":"estimate.amount_cents","typeLabel":"number\u003cdouble>"},"credits":{"description":"`amount` in Comfy credits using the platform conversion ($1 = 211 credits), rounded to 2 decimals - the figure `X-Comfy-Credits-Used` is expected to carry for an `exact` quote. That header is a rounded post-hoc figure, not a receipt; the settled charge in your workspace's usage and billing records is final.","format":"double","type":"number","uniqueKey":"estimate.credits","typeLabel":"number\u003cdouble>"},"max_amount":{"description":"The high end of the estimate in dollars as a decimal string. It is an estimate, not a guaranteed cap: a call can be billed more, for example when the provider returns more than was asked (such as more images than requested), when the estimate is learned from past calls, or when the price does not rise steadily with the amount requested. Present only when `source` is `estimated`.","type":"string","uniqueKey":"estimate.max_amount","typeLabel":"string"},"max_amount_cents":{"description":"`max_amount` in US cents. Present with `max_amount`.","format":"double","type":"number","uniqueKey":"estimate.max_amount_cents","typeLabel":"number\u003cdouble>"},"min_amount":{"description":"The low end of the estimate in dollars as a decimal string. It is an estimate, not a guaranteed floor: a call can be billed less, for example when the provider returns less than the request asked for (such as no images), when the estimate is a typical price learned from past calls, or when the price does not rise steadily with the amount requested. Present only when `source` is `estimated`.","type":"string","uniqueKey":"estimate.min_amount","typeLabel":"string"},"min_amount_cents":{"description":"`min_amount` in US cents, unrounded. Present with `min_amount`.","format":"double","type":"number","uniqueKey":"estimate.min_amount_cents","typeLabel":"number\u003cdouble>"},"reason":{"description":"Present only when `source` is `unknown`: `not_quotable` (Router cannot price this call before it runs - its price depends on what the provider reports back, or the body does not bound a billed quantity), `unpriced_model` (the rate card does not price this request), `byok` (queued submit only: the run uses your own provider key, so Comfy bills nothing for it), or `pricing_unavailable` (queued submit only: the rate card could not be read when the request was admitted; this is transient, and the run is still charged as usual). Read any other value as `not_quotable`.","type":"string","uniqueKey":"estimate.reason","typeLabel":"string"}},"uniqueKey":"estimate","typeLabel":"object"},"queue_position":{"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.\nIT 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.\nIt 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","uniqueKey":"queue_position","typeLabel":"integer"}},"uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"cancel_url":"https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/cancel","response_url":"https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21","status_url":"https://api.comfy.org/v2/models/bfl/flux-pro-1.1/requests/6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21/status","request_id":"6f1a1a6e-6a53-4a5f-9d3a-2b3b0a1f9c21","status":"IN_QUEUE","estimate":{"currency":"\u003cstring>","model":"bfl/flux-2-pro","pricing_as_of":"2023-11-07T05:31:56Z","provider":"\u003cstring>","source":"\u003cstring>","amount":"\u003cstring>","amount_cents":123,"credits":123,"max_amount":"\u003cstring>","max_amount_cents":123,"min_amount":"\u003cstring>","min_amount_cents":123,"reason":"\u003cstring>"},"queue_position":3}}},"exampleType":"generated"}},"description":"Created - the run was admitted to the queue. The body is the handle: the `request_id`, `status: IN_QUEUE`, a `queue_position` snapshot, and the `status_url`, `response_url` and `cancel_url` that address the rest of this request's lifetime. It is deliberately `201` and not `202`: a queued request is a resource this call created and the three URLs address it, whereas the `202` on the result read below means \"not ready yet, ask again\" and creates nothing.","headers":{"Idempotent-Replayed":{"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"}},"X-Comfy-Request-Id":{"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.\nIt 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"}},"X-Comfy-Router-Dropped-Params":{"description":"One JSON-encoded string holding an array of strings - decode it with a JSON parser rather than splitting it on commas, because it is a single string on the wire, not a comma-separated OpenAPI array, and each entry is a sentence carrying commas of its own - present whenever a translation produced this call's request body and could not express one or more native fields exactly on the provider that served it, naming each dropped field and why, whether the caller asked for that translation with `model_provider` (`strict_mode=false`, the default) or an automatic `fallback_provider` retry ran it. Absent when no translation ran, when translation ran but dropped nothing, and on an error response. On a queued submit it is on the `201` and any same-key replay of it, naming what the `model_provider` leg will drop; the queued status and result responses do not carry it. On a fallback retry it names what that retry's own translation (into the provider that actually served the call) dropped, never the primary attempt's. The disclosure is bounded three ways, so that it stays a fixed size rather than one that grows with the request body — some native arrays (`input.media` on the Wan video family) are deliberately uncapped, and entries quote caller-supplied values: at most 64 entries, each at most 300 bytes plus an elision mark, summing to at most 4096 bytes of the encoded header value. Whichever bound binds first wins. Shortening is never silent: an entry cut to the per-entry bound ends in `…`, and when entries are left out entirely a final summarising entry states how many. Treat that count as a lower bound on the fields affected rather than a tally of them: it counts disclosure entries, and a translator may collapse many dropped elements into one entry (the Wan video family's media groups do exactly that). Entries are also sanitised before publication — a media URL is stripped of its query string, which is what carries its credential — so an entry names the field it dropped rather than reproducing the value verbatim. It is stored with the `Idempotency-Key` record and replayed unchanged on a same-key retry (alongside `Idempotent-Replayed: true`), so a retry carries the same disclosure the original call did rather than reading as a drop-free run. In the one case where the stored headers were too large to keep in full, the record is marked non-replayable and the retry is refused with `409` rather than served without this header - a refusal a caller can act on, where a replay that silently carried no disclosure is the outcome this header exists to prevent.","schema":{"example":"[\"moderation (fal applies its own, non-configurable safety filtering)\"]","type":"string"}}},"exampleType":"generated"},"0e48f5ac-3f62-41ec-8c98-c2acd9b2bb3d":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"7fc087b1-e2bd-42d9-a1eb-a50c9396413b":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"338c7010-a706-4c6f-a594-2e98b7c99ebc":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"6f93f168-c268-43a7-9e7c-4d685bedd453":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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`. On this route `not_enabled` also answers a model whose partner answers a generation directly as bytes: such a model cannot yet be queued, so the submit refuses it and nothing is queued or charged. For that refusal it is the model and not the account that is refused: run such a model on the synchronous route, `POST /v2/models/{provider}/{model}`, instead. The same redirect is given, in `detail`, to a bring-your-own-key request whose credential resolved but that the queue cannot carry. A credential that carries no Comfy workspace is refused `not_enabled` here too, with a `detail` naming the credential, while the synchronous route accepts it. Any other `not_enabled` or `forbidden` here is refused by the synchronous route the same way, so read `detail` rather than the bucket before re-sending elsewhere.","headers":{"X-Comfy-Error-Type":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"e2f18ce0-b497-410e-9d1d-142270762628":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"353347b3-8126-4f9e-83b7-825d9a2e8ffa":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"description":"This request cannot be served as sent, and on `POST /v2/models/{provider}/{model}` three unrelated conditions answer this status. The first, which every route declaring this status can raise: the `Idempotency-Key` on this request is already held, and this request cannot be answered from its record. Two buckets share the status there and `X-Comfy-Error-Type` is what separates them, because they are acted on in opposite ways. `concurrency_limit_exceeded` means the original call for this key is still running: wait `Retry-After` seconds and re-send the same key, which collects that call's result rather than starting a second one. `invalid_input` means the key cannot serve this request at all - it was already used for a different request (the method, the path and query, or the body differ from the original; a body that differs only in the signature of a signed Comfy asset URL is not different, unless the original's recorded answer was an error), or the original completed (and, if it succeeded, was charged) and Router holds no copy of its response it can still stand behind - for example it was too large to store, or it names an asset Comfy does not host and so cannot promise still resolves, which on a direct-return model is replayed for a few minutes after the original call and refused after that - or the copy it holds is content-encoded in a way this request did not accept - and the answer for the key is always a new key, never a re-send of this one. There is no `Retry-After` on any of these, because waiting changes nothing. `detail` says which case it is; the different-request case says nothing about how the call that does own the key turned out. The second and third conditions are raised only by `POST /v2/models/{provider}/{model}`, are not about the `Idempotency-Key` at all, and are both reachable on a request that carries no key. Router checks the third before the second, so a request that would trip both is refused for the third: an explicit `model_provider` naming an alternate provider on a request whose body selects a multipart operation (an edit, for example gpt-image's `image` field), when that provider's translator for this model cannot itself serve the operation - the swap is refused rather than silently billing a plain generation for the edit the caller actually asked for. A leg whose translator does carry the media is not refused and proceeds normally, so this refusal is per-leg rather than blanket. This one is not raised by the automatic on-failure retry `fallback_provider` controls, which is simply skipped (inert, not refused) on any multipart body, whether or not a leg could have served it - see the `model_provider` parameter. Its `detail` begins \"this request's `image` selects the edit operation\". The second: `model_provider` named an alternate provider on a request that had already resolved a bring-your-own-key credential for the provider in the path. The three are mutually exclusive - the multipart case is decided from the request body and the named leg's own translator, before BYOK is even considered - and the BYOK case is exclusive with the key case because that credential was resolved for the path's provider while every alternate leg dispatches on Comfy's own key for its own provider, so the call is refused before anything is dispatched and nothing is charged. Both the second and third carry `invalid_input`, the same bucket as the terminal key case above, so `X-Comfy-Error-Type` does not separate any of the three and `detail` is what a client branches on: the BYOK case begins `this request resolved a BYOK credential`, and a new key does not help - drop `model_provider`, or send the request without the BYOK credential. See the `model_provider` parameter, which also records that `fallback_provider` is inert rather than refused on a BYOK request. The body is `RouterErrorResponse` and the bucket is repeated on `X-Comfy-Error-Type`.","headers":{"Retry-After":{"description":"Seconds to wait before retrying the same request with the same `Idempotency-Key`. It is set on the two answers such a retry can actually collect from: a `409` carrying `error_type: concurrency_limit_exceeded`, where the original call for that key is still running, and a `deadline_exceeded` `504`, where Comfy stopped holding the connection but still holds a handle to a generation the provider is running. In both cases the value is the interval Router itself would wait before asking again, which is the one honest number this route has for \"ask again later\". Absent when there is nothing to collect: an unkeyed call, a bound that expired before the provider accepted anything, or a `409` that refuses the key outright instead of asking the caller to wait.","schema":{"example":2,"minimum":1,"type":"integer"}},"X-Comfy-Error-Type":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"8504d484-d48e-4d6c-a6e8-8c0ebacdebe9":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"aa5160a7-f935-4684-9be4-c529d5145b01":{"content":{"application/json":{"schema":{"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":{"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":{"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":{"oneOf":[{"type":"string","uniqueKey":"detail.items.[INDEX].loc.items.[INDEX].oneOf.0","typeLabel":"string"},{"type":"integer","uniqueKey":"detail.items.[INDEX].loc.items.[INDEX].oneOf.1","typeLabel":"integer"}],"uniqueKey":"detail.items.[INDEX].loc.items.[INDEX]","typeLabel":"string | integer"},"type":"array","isRequired":true,"uniqueKey":"detail.items.[INDEX].loc","typeLabel":"(string | integer)[]"},"msg":{"description":"Human-readable description of this single failure.","type":"string","isRequired":true,"uniqueKey":"detail.items.[INDEX].msg","typeLabel":"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","isRequired":true,"uniqueKey":"detail.items.[INDEX].type","typeLabel":"string"},"ctx":{"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","uniqueKey":"detail.items.[INDEX].ctx","typeLabel":"object"},"input":{"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.","uniqueKey":"detail.items.[INDEX].input","typeLabel":"any"}},"required":["loc","msg","type"],"type":"object","uniqueKey":"detail.items.[INDEX]","typeLabel":"object"},"type":"array","isRequired":true,"uniqueKey":"detail","typeLabel":"object[]"}},"required":["detail"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":[{"loc":["\u003cstring>"],"msg":"\u003cstring>","type":"image_too_small","ctx":{},"input":"\u003cunknown>"}]}}},"exampleType":"generated"}},"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\"]`.\nWhat 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.\nOn 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.\nA 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":{"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"}},"X-Comfy-Error-Type":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"742d7b37-e4a3-4f10-8aef-3297eddcf791":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"description":"Inspect `X-Comfy-Error-Type`: `queue_backlog_full` means you already hold as many unfinished queued requests as you are allowed, so submit again once one of them finishes; `rate_limited` means wait the number of seconds in `Retry-After`. Nothing was queued or charged, and the `Idempotency-Key` is not consumed. A submit at your in-flight limit is queued rather than refused, so `concurrency_limit_exceeded` is never answered here.","headers":{"Retry-After":{"description":"Seconds to wait before sending another request after a `429` carrying `error_type: rate_limited`: the time until the caller's per-minute request allowance has refilled enough to admit one more request. It is sent on every `rate_limited` refusal and on no other answer, so it is absent on a `429` whose `X-Comfy-Error-Type` is `concurrency_limit_exceeded` or `queue_backlog_full`. Nothing the caller does drains it early; a request sent sooner is refused again and spends nothing, but gains nothing either.","schema":{"example":2,"minimum":1,"type":"integer"}},"X-Comfy-Error-Type":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"94ec999d-fafd-4dd7-8ac8-54eb1e24cf18":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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`. A `503` that was refused for capacity - the in-flight request-body budget was full, or no slot was free to scan an oversized body - carries `Retry-After` naming when to re-send the identical request; a `503` raised because a dependency faulted does not, because none of those rails knows when it will recover.","headers":{"Retry-After":{"description":"Seconds to wait before re-sending the same request, unchanged. It is present only when Router refused the request body for capacity - the in-flight request-body budget was full, or no slot was free to scan an oversized body - and a capacity refusal submitted nothing and charged nothing, so re-sending the identical call once the interval has passed is the whole remedy. There is no `Idempotency-Key` to collect under and no queued request to poll for; this is the one `Retry-After` on Router that means simply \"send it again later\". It is absent on the dependency-fault `503`s that share this status - a credential rail, an idempotency store or a policy decision point that could not answer - because none of them knows when it will, and a wrong number is worse than none against a dependency that is already failing.\nRead its absence as \"no interval published\", never as \"retry immediately\": that `503` is still worth retrying, just on the client's own backoff instead of Router's.","schema":{"example":4,"minimum":1,"type":"integer"}},"X-Comfy-Error-Type":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"},"a6b5a441-5a59-4f2b-8002-9a6ea08fd9a3":{"content":{"application/json":{"schema":{"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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and visible to the caller, or just `\u003cid> was retired on \u003cYYYY-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.\nTwo 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","isRequired":true,"uniqueKey":"detail","typeLabel":"string"},"error_type":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}],"isRequired":true,"uniqueKey":"error_type","typeLabel":"string"},"billed_workspace":{"description":"The wallet a refused call would have charged, on `RouterErrorResponse.billed_workspace`. Answers \"which wallet is empty?\" - it is not necessarily the workspace the caller's UI currently has selected, and a team member's personal credential names their personal wallet here even while a funded team is open. Present only alongside `denied_reason`.","properties":{"type":{"description":"`personal` or `team`. An open string: treat any other value as personal.","type":"string","isRequired":true,"uniqueKey":"billed_workspace.type","typeLabel":"string"},"id":{"description":"The Cloud workspace id. Absent on the legacy rail.","type":"string","uniqueKey":"billed_workspace.id","typeLabel":"string"},"rail":{"description":"`legacy` when the call bills the caller's pre-workspace Metronome customer rather than a Cloud workspace; that customer has no workspace id. Absent for a Cloud workspace.","type":"string","uniqueKey":"billed_workspace.rail","typeLabel":"string"}},"required":["type"],"type":"object","uniqueKey":"billed_workspace","typeLabel":"object"},"credential_kind":{"description":"How the refused request authenticated - `personal_api_key`, `team_api_key`, `personal_session` or `team_session`. Answers \"which credential do I switch to bill the other wallet?\". It is not a permission and grants nothing. Present only alongside `denied_reason`.","type":"string","uniqueKey":"credential_kind","typeLabel":"string"},"denied_reason":{"description":"Billing's policy branch for a partner-node spend denial - `no_funds` when the billed wallet is empty, `unspecified` when the call was refused without a funds reading saying so. Present only when `error_type` is `insufficient_credits` and the deployment reports wallet facts; absent otherwise. A machine-readable code, not display text: show `detail`, branch on this.","type":"string","uniqueKey":"denied_reason","typeLabel":"string"},"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","uniqueKey":"refusal_subject","typeLabel":"string"},"upstream_detail":{"description":"A bounded, sanitized reason for an `invalid_input` rejection. On the synchronous route it is the reason the model provider gave, 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). There it is absent on every other failure, including provider `5xx`, transport failures, content-policy refusals and any refusal Router raised about itself, and it mirrors the `X-Comfy-Upstream-Detail` header. The queued collect (`GET /v2/models/{provider}/{model}/requests/{request_id}`) of a run that ended `invalid_input` carries the same reason in this field on its first collect, which then deletes it from the request: a later collect normally answers the same `error_type` and `detail` without it, so read it from the first collect. That read sends neither header. The queued collect also uses this field for Router's own reason when Router refused the request before sending it to the provider and can state why in one sentence, which the synchronous route answers in `detail`; a refusal that names individual fields carries none.\nUnlike `detail`, this text is provider-authored, or on the queued collect possibly Router's own sentence, and either way 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","uniqueKey":"upstream_detail","typeLabel":"string"}},"required":["detail","error_type"],"type":"object","uniqueKey":"","typeLabel":"object"},"examples":{"Example":{"title":"Example","value":{"detail":"\u003cstring>","error_type":"invalid_input","billed_workspace":{"type":"\u003cstring>","id":"\u003cstring>","rail":"\u003cstring>"},"credential_kind":"\u003cstring>","denied_reason":"\u003cstring>","refusal_subject":"\u003cstring>","upstream_detail":"\u003cstring>"}}},"exampleType":"generated"}},"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":{"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":{"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 twenty 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`, `queue_backlog_full` and `result_expired`. 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.\nRETRY 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.\n`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.\nHOW 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.\n`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\".\nNEITHER 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.\n`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.\nIt 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 `\u003cid> was retired on \u003cYYYY-MM-DD>; use \u003csuccessor>` when a successor is recorded and not withheld, or just `\u003cid> was retired on \u003cYYYY-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, and `Retry-After` the seconds to wait.","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"},{"meaning":"The request existed and finished, but its result is no longer stored, so the collect has nothing to return. It is returned under `410` and is permanent for that id: re-reading will answer the same way, so do not retry it. It is deliberately not `request_not_found`, which would tell you the id was never valid and invite re-submitting a generation that has already run. Running the model again means submitting a new request. For a workspace under a zero-data-retention agreement, a queued request's result is not kept after it is delivered: it is removed shortly after the first collect, and an uncollected result is removed after a grace period once the request finishes, so store the result on your side when you first collect it.","tier":"transport","value":"result_expired"}]}},"X-Comfy-Request-Id":{"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.\nIt 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"}}},"exampleType":"generated"}},"security":{"84ebca82-60bf-47a3-ac72-11fd5e2f5b57":{"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.\nAccepted on the operations served by the comfyFirebase auth middleware (BE-9720, parity with ingest).","scheme":"bearer","type":"http","scopes":[],"schemeName":"BearerAuth"}},"2d7e774d-cee7-4b74-8eec-ecf2ba2bcc38":{"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.\nThe 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","scopes":[],"schemeName":"ApiKeyAuth"}}},"servers":{"0579de4e-d11b-4f34-98ad-304ea266ca73":{"url":"https://api.comfy.org"}},"processedSecurityOptions":[{"schemes":[{"scheme":{"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.\nAccepted on the operations served by the comfyFirebase auth middleware (BE-9720, parity with ingest).","scheme":"bearer","type":"http","scopes":[],"schemeName":"BearerAuth"},"schema":{"title":"Bearer Auth","type":"string","isRequired":true,"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.\nAccepted on the operations served by the comfyFirebase auth middleware (BE-9720, parity with ingest).","format":"bearer","typeLabel":"string\u003cbearer>","deprecated":false},"schemeName":"BearerAuth","name":"Authorization"}],"title":"BearerAuth"},{"schemes":[{"scheme":{"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.\nThe 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","scopes":[],"schemeName":"ApiKeyAuth"},"schema":{"title":"X-API-Key","type":"string","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.\nThe 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.","isRequired":true,"typeLabel":"string","deprecated":false},"schemeName":"ApiKeyAuth","name":"X-API-Key"}],"title":"ApiKeyAuth"}]},"originalFileLocation":"https://api.comfy.org/openapi"},"subdomain":"dripart","siteName":"ComfyUI","title":"Submit a partner model run to the queue and return immediately. - ComfyUI","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{"en":{"href":"/","matchLevel":4},"zh":{"href":"/zh","matchLevel":4},"ja":{"href":"/ja","matchLevel":4},"ko":{"href":"/ko","matchLevel":4},"es":{"href":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately","matchLevel":0}}},"lastModified":"2026-11-13T10:29:52.881Z","originalFileLocation":"es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately.mdx","locale":"es"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"dark\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Comfy Router\u0026#x27;s queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below. Choose it over the synchronous route when the model\u0026#x27;s generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion. ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403 not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key\u0026#x27;s workspace, the workspace is unknown rather than absent, and the answer is a retryable 503 service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402 insufficient_credits, whatever their balance, because adding credits would not change the answer. The 201 carries request_id and the three URLs that address the rest of this request\u0026#x27;s lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move. request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel. READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand. AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call\u0026#x27;s own id in the header. Idempotent-Replayed: true marks exactly those responses. So request_id is authoritative on every response. Keep the body field. This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.\">\u003cmeta data-mint-head name=\"application-name\" content=\"ComfyUI\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Submit a partner model run to the queue and return immediately. - ComfyUI\">\u003cmeta data-mint-head property=\"og:description\" content=\"Comfy Router\u0026#x27;s queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below. Choose it over the synchronous route when the model\u0026#x27;s generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion. ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403 not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key\u0026#x27;s workspace, the workspace is unknown rather than absent, and the answer is a retryable 503 service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402 insufficient_credits, whatever their balance, because adding credits would not change the answer. The 201 carries request_id and the three URLs that address the rest of this request\u0026#x27;s lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move. request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel. READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand. AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call\u0026#x27;s own id in the header. Idempotent-Replayed: true marks exactly those responses. So request_id is authoritative on every response. Keep the body field. This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"ComfyUI\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://dripart.mintlify.site/_mintlify/api/og/v2?division=Comfy+Router\u0026amp;title=Submit+a+partner+model+run+to+the+queue+and+return+immediately.\u0026amp;description=Comfy+Router%27s+queued+delivery+mode.+The+request+body+is+the+same+partner-native+JSON+input+%60POST+%2Fv2%2Fmodels%2F%7Bprovider%7D%2F%7Bmodel%7D%60+accepts+for+this+model+-+one+bo\u0026amp;theme=b252c39adf72ba65053766a0\u0026amp;signature=f7538887f795d1eda401f26e98648008de71e7da4251f1c282b68a248dfdecb7\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Submit a partner model run to the queue and return immediately. - ComfyUI\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Comfy Router\u0026#x27;s queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below. Choose it over the synchronous route when the model\u0026#x27;s generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion. ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403 not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key\u0026#x27;s workspace, the workspace is unknown rather than absent, and the answer is a retryable 503 service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402 insufficient_credits, whatever their balance, because adding credits would not change the answer. The 201 carries request_id and the three URLs that address the rest of this request\u0026#x27;s lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move. request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel. READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand. AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call\u0026#x27;s own id in the header. Idempotent-Replayed: true marks exactly those responses. So request_id is authoritative on every response. Keep the body field. This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://dripart.mintlify.site/_mintlify/api/og/v2?division=Comfy+Router\u0026amp;title=Submit+a+partner+model+run+to+the+queue+and+return+immediately.\u0026amp;description=Comfy+Router%27s+queued+delivery+mode.+The+request+body+is+the+same+partner-native+JSON+input+%60POST+%2Fv2%2Fmodels%2F%7Bprovider%7D%2F%7Bmodel%7D%60+accepts+for+this+model+-+one+bo\u0026amp;theme=b252c39adf72ba65053766a0\u0026amp;signature=f7538887f795d1eda401f26e98648008de71e7da4251f1c282b68a248dfdecb7\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/dripart/lS3H5I41bE6LSXEf/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"ComfyUI\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#49378B\">\u003cmeta data-mint-head name=\"googlebot\" content=\"max-video-preview:0\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.comfy.org/#organization\",\"name\":\"ComfyUI\",\"url\":\"https://docs.comfy.org\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/dripart/WCFdTagRdhtvk9-o/logo/light.svg?fit=max\u0026auto=format\u0026n=WCFdTagRdhtvk9-o\u0026q=85\u0026s=ec112de87239058f5a931a7e4b594965\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.comfy.org#website\",\"name\":\"ComfyUI\",\"url\":\"https://docs.comfy.org\",\"publisher\":{\"@id\":\"https://docs.comfy.org/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately#webpage\",\"url\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately\",\"name\":\"Submit a partner model run to the queue and return immediately.\",\"description\":\"Comfy Router's queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below. Choose it over the synchronous route when the model's generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion. ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403 not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key's workspace, the workspace is unknown rather than absent, and the answer is a retryable 503 service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402 insufficient_credits, whatever their balance, because adding credits would not change the answer. The 201 carries request_id and the three URLs that address the rest of this request's lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move. request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel. READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand. AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call's own id in the header. Idempotent-Replayed: true marks exactly those responses. So request_id is authoritative on every response. Keep the body field. This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.\",\"dateModified\":\"2026-11-13T10:29:52.881Z\",\"isPartOf\":{\"@id\":\"https://docs.comfy.org#website\"},\"breadcrumb\":{\"@id\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Nodos personalizados\",\"item\":\"https://docs.comfy.org/es/custom-nodes/intro\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Package \u0026 Publish\",\"item\":\"https://docs.comfy.org/es/custom-nodes/help_page\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Registry API Reference\",\"item\":\"https://docs.comfy.org/es/registry/api-reference/admin/generate-a-short-lived-jwt-admin-token\"},{\"@type\":\"ListItem\",\"position\":4,\"name\":\"Comfy Router\",\"item\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/list-the-models-comfy-router-can-run\"},{\"@type\":\"ListItem\",\"position\":5,\"name\":\"Submit a partner model run to the queue and return immediately.\",\"item\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately\"}]},{\"@type\":[\"TechArticle\",\"APIReference\"],\"@id\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately#apireference\",\"headline\":\"Submit a partner model run to the queue and return immediately.\",\"name\":\"Submit a partner model run to the queue and return immediately.\",\"description\":\"Comfy Router's queued delivery mode. The request body is the same partner-native JSON input POST /v2/models/{provider}/{model} accepts for this model - one body shape, one per-model schema, two delivery modes - but this route does not hold the connection for the result. It admits the run, answers 201 with a handle, and the caller collects the result later through the three reads below. Choose it over the synchronous route when the model's generations outlast a connection Comfy is willing to hold, or when the caller has no thread to spare waiting. Everything else about the call is identical: the same model ID, the same input schema, the same enablement gate, the same entitlement and funds checks, and the same charge on completion. ONE CREDENTIAL REQUIREMENT IS NOT SHARED WITH THE SYNCHRONOUS ROUTE. The queue is scoped per workspace, so this route and the three reads below need a credential that is attached to a Comfy workspace: a workspace-scoped Cloud token, or an API key the platform resolves to a workspace. A credential that carries no workspace - a plain Firebase token, or some API keys on a workspace still on the legacy billing rail - is refused with 403 not_enabled and a detail that says the credential is the cause, distinct from the rollout refusal. The synchronous route accepts that credential. When the platform could not reach the service that resolves an API key's workspace, the workspace is unknown rather than absent, and the answer is a retryable 503 service_unavailable instead. On this route the credential check comes before the funds check: a caller whose credential carries no workspace gets the 403, not a 402 insufficient_credits, whatever their balance, because adding credits would not change the answer. The 201 carries request_id and the three URLs that address the rest of this request's lifetime, so a client never has to compose a queue URL itself. It also carries queue_position as a courtesy at admission time; it is a snapshot, not a promise, and the status read below is where a client watches it move. request_id IS NOT X-Comfy-Request-Id - not on a first submit and not on any other response. They are two identifiers with two jobs. request_id is the DURABLE handle for the queued request, minted with the row and unchanged for its whole life. X-Comfy-Request-Id is the id of the individual HTTP CALL: the server mints one per call and re-asserts it when the response commits, so it is one value on this submit, a different one on each poll, and a different one again on the cancel. READ THE HANDLE FROM THE BODY. A client that keeps only the header polls a request that does not exist. The three URLs above already embed the handle, so a client that follows them never has to hold either id by hand. AN IDEMPOTENT REPLAY CHANGES NOTHING ABOUT THAT; it is worth spelling out only because it is where the mistake is loudest. RouterIdempotencyMiddleware replays the recorded body but not the recorded request id: its replay allowlist enumerates response headers deliberately and excludes per-request ones, so a replayed call still gets an id of its own. A replayed submit therefore returns the ORIGINAL handle in request_id and, exactly as on every other response, this call's own id in the header. Idempotent-Replayed: true marks exactly those responses. So request_id is authoritative on every response. Keep the body field. This operation is deliberately tagged Comfy Router and NOT API Nodes, for the reason given on the synchronous route.\",\"url\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately\",\"mainEntityOfPage\":{\"@id\":\"https://docs.comfy.org/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately#webpage\"},\"dateModified\":\"2026-11-13T10:29:52.881Z\",\"publisher\":{\"@id\":\"https://docs.comfy.org/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.comfy.org#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"ComfyUI\",\"publisher\":{\"@type\":\"Organization\",\"name\":\"ComfyUI\"},\"provider\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","agentPointerHref":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately.md","siteName":"ComfyUI","nav":{"global":null,"languages":[{"language":"en","pages":[{"title":"en","href":"/"}]},{"language":"zh","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"资源","items":[{"label":"安装","href":"/zh/installation/system_requirements"},{"label":"教程","href":"/zh/tutorials/basic/text-to-image"},{"label":"开发","href":"/zh/development/overview"}]},{"header":"产品","items":[{"label":"功能","href":"https://www.comfy.org/?utm_source=docs"},{"label":"画廊","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"下载","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"公司","items":[{"label":"关于","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"招聘","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"服务条款","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"隐私政策","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"pages":[{"title":"zh","href":"/zh"}]},{"language":"ja","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"リソース","items":[{"label":"インストール","href":"/ja/installation/system_requirements"},{"label":"チュートリアル","href":"/ja/tutorials/basic/text-to-image"},{"label":"開発","href":"/ja/development/overview"}]},{"header":"プロダクト","items":[{"label":"機能","href":"https://www.comfy.org/?utm_source=docs"},{"label":"ギャラリー","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"ダウンロード","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"会社情報","items":[{"label":"概要","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"採用情報","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"利用規約","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"プライバシーポリシー","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"navbar":{"links":[{"href":"/ja/tutorials/partner-nodes/pricing","label":"料金"},{"href":"https://comfy.org/download?utm_source=docs","label":"ダウンロード"}],"primary":{"type":"button","label":"Comfy Cloud","href":"https://comfy.org/cloud?utm_source=docs"}},"pages":[{"title":"ja","href":"/ja"}]},{"language":"ko","footer":{"socials":{"github":"https://github.com/Comfy-Org/ComfyUI","x":"https://x.com/ComfyUI","discord":"https://discord.com/invite/comfyorg","youtube":"https://www.youtube.com/@comfyorg"},"links":[{"header":"리소스","items":[{"label":"설치","href":"/ko/installation/system_requirements"},{"label":"튜토리얼","href":"/ko/tutorials/basic/text-to-image"},{"label":"개발","href":"/ko/development/overview"}]},{"header":"제품","items":[{"label":"기능","href":"https://www.comfy.org/?utm_source=docs"},{"label":"갤러리","href":"https://www.comfy.org/gallery?utm_source=docs"},{"label":"다운로드","href":"https://www.comfy.org/download?utm_source=docs"}]},{"header":"회사 정보","items":[{"label":"소개","href":"https://www.comfy.org/about?utm_source=docs"},{"label":"채용","href":"https://www.comfy.org/careers?utm_source=docs"},{"label":"이용약관","href":"https://www.comfy.org/terms-of-service?utm_source=docs"},{"label":"개인정보 처리방침","href":"https://www.comfy.org/privacy-policy?utm_source=docs"}]}]},"navbar":{"links":[{"href":"/ko/tutorials/partner-nodes/pricing","label":"가격 정책"},{"href":"https://comfy.org/download?utm_source=docs","label":"다운로드"}],"primary":{"type":"button","label":"Comfy Cloud","href":"https://comfy.org/cloud?utm_source=docs"}},"pages":[{"title":"ko","href":"/ko"}]},{"language":"es","tabs":[{"tab":"Empezar","pages":[{"title":"Empezar","href":"/es"}]},{"tab":"Tutoriales","pages":[{"title":"Tutoriales","href":"/es/basic-concepts/workflow"}]},{"tab":"Comfy Router","pages":[{"title":"Comfy Router","href":"/es/development/comfy-router/quickstart"}]},{"tab":"Herramientas para agentes","pages":[{"title":"Herramientas para agentes","href":"/es/agent-tools"}]},{"tab":"Nodos integrados","pages":[{"title":"Nodos integrados","href":"/es/built-in-nodes/overview"}]},{"tab":"Desarrollo","pages":[{"group":"Desarrollar con Comfy","pages":[{"title":"ComfyUI para desarrolladores: SDK, API de servidor y nube","sidebarTitle":"Descripción general para desarrolladores","href":"/es/development/overview"},{"title":"Obtener una clave API","sidebarTitle":"Claves API","icon":"key","href":"/es/development/api-development/getting-an-api-key"},{"title":"Ejemplos de Comfy API y Router","sidebarTitle":"Ejemplos","icon":"code","mode":"wide","href":"/es/development/examples"}]},{"group":"Desplegar ComfyUI","pages":[{"title":"Implementar ComfyUI","sidebarTitle":"Descripción general","icon":"map","href":"/es/development/deploy/overview"},{"group":"API de Comfy","pages":[{"title":"Inicio rápido de Comfy API","sidebarTitle":"Inicio rápido","icon":"cloud-arrow-up","href":"/es/development/serverless/quickstart"},{"title":"Otras formas de crear un Build de Comfy API","sidebarTitle":"Fuentes del Build","icon":"layer-group","href":"/es/development/serverless/build-sources"},{"title":"Guía de despliegue de Comfy API","sidebarTitle":"Guía de despliegue","icon":"cloud-arrow-up","href":"/es/development/serverless/overview"}]},{"group":"Comfy Cloud","pages":[{"title":"Inicio rápido de Comfy Cloud","sidebarTitle":"Inicio rápido","icon":"cloud","href":"/es/development/deploy/cloud"},{"title":"Uso y límites","sidebarTitle":"Uso y límites","icon":"gauge","href":"/es/development/deploy/cloud-usage"}]},{"group":"Opciones de autoalojamiento","pages":[{"title":"Opciones de autoalojamiento","sidebarTitle":"Opciones de alojamiento","icon":"server","href":"/es/development/deploy/self-hosting"},{"group":"Self-Hosted Server API","pages":[{"title":"Descripción general del servidor","sidebarTitle":"Descripción general del servidor","href":"/es/development/comfyui-server/comms_overview"},{"title":"Ejemplos de la API de ComfyUI Server: HTTP, WebSocket e historial","sidebarTitle":"Ejemplos de API","href":"/es/development/comfyui-server/api-examples"},{"title":"Rutas del servidor de ComfyUI (API HTTP y WebSocket)","sidebarTitle":"Rutas HTTP y WebSocket","icon":"route","href":"/es/development/comfyui-server/comms_routes"},{"title":"Mensajes del servidor de ComfyUI (API WebSocket)","sidebarTitle":"Mensajes de WebSocket","icon":"comment","href":"/es/development/comfyui-server/comms_messages"},{"title":"Flags de inicio de ComfyUI: referencia de línea de comandos","sidebarTitle":"Flags de inicio","icon":"terminal","href":"/es/development/comfyui-server/startup-flags"},{"title":"Integración de la clave API de la cuenta de ComfyUI","sidebarTitle":"Integración de clave API","icon":"puzzle-piece","href":"/es/development/comfyui-server/api-key-integration"}]}]},{"group":"Ejecutar flujos de trabajo","pages":[{"title":"Ejecutar flujos de trabajo","sidebarTitle":"Descripción general","icon":"play","href":"/es/development/run-workflows/overview"},{"title":"Ejecuta tu primer flujo de trabajo","sidebarTitle":"Primer flujo de trabajo","href":"/es/development/api-development/quickstart"},{"title":"SDK de Comfy: clientes de Python y TypeScript para ComfyUI","sidebarTitle":"SDK","icon":"code","href":"/es/development/api-development/sdks"},{"title":"Proxy de API para ComfyUI autoalojado","sidebarTitle":"Proxy de API","icon":"plug","href":"/es/development/comfyui-server/api-proxy"},{"title":"Formato de API de flujos de trabajo","icon":"file-code","href":"/es/development/api-development/workflow-api-format"},{"title":"Metadatos de flujos de trabajo en archivos de salida de ComfyUI","sidebarTitle":"Metadatos de flujos","icon":"file-code","href":"/es/development/api-development/workflow-metadata"},{"group":"Comfy API Reference","boost":10,"pages":[{"title":"Versiones de Comfy API: v2 y la API v1 de Cloud obsoleta","sidebarTitle":"Descripción general","href":"/es/development/comfy-api/overview"},{"group":"v2","pages":[{"title":"Descripción general de Comfy API v2","sidebarTitle":"Descripción general","href":"/es/api-reference/v2/overview"},{"title":"Notas de diseño","sidebarTitle":"Notas de diseño","icon":"compass","href":"/es/development/api-development/sdks-design"},{"group":"Referencia de la API","pages":[{"group":"assets","pages":[{"title":"Upload an asset (single-call multipart)","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml post /api/v2/assets","href":"/es/api-reference/v2/assets/upload-an-asset-single-call-multipart"},{"title":"Create or reuse an asset over existing bytes","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml post /api/v2/assets/from-hash","href":"/es/api-reference/v2/assets/create-or-reuse-an-asset-over-existing-bytes"},{"title":"Existence check for a blob by blake3 hash","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml head /api/v2/assets/by-hash/{hash}","href":"/es/api-reference/v2/assets/existence-check-for-a-blob-by-blake3-hash"},{"title":"Asset metadata","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/assets/{id}","href":"/es/api-reference/v2/assets/asset-metadata"},{"title":"Delete an asset record","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml delete /api/v2/assets/{id}","href":"/es/api-reference/v2/assets/delete-an-asset-record"},{"title":"Asset bytes","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/assets/{id}/content","href":"/es/api-reference/v2/assets/asset-bytes"}]},{"group":"jobs","pages":[{"title":"List jobs, newest first","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/jobs","href":"/es/api-reference/v2/jobs/list-jobs-newest-first"},{"title":"Submit a workflow for execution","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml post /api/v2/jobs","href":"/es/api-reference/v2/jobs/submit-a-workflow-for-execution"},{"title":"Job status (the polling workhorse)","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/jobs/{id}","href":"/es/api-reference/v2/jobs/job-status-the-polling-workhorse"},{"title":"The workflow behind a job — authoring version if pinned, executed graph otherwise","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/jobs/{id}/workflow","href":"/es/api-reference/v2/jobs/the-workflow-behind-a-job-—-authoring-version-if-pinned-executed-graph-otherwise"},{"title":"What the run printed","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/jobs/{id}/logs","href":"/es/api-reference/v2/jobs/what-the-run-printed"},{"title":"Live event stream (SSE)","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml get /api/v2/jobs/{id}/events","href":"/es/api-reference/v2/jobs/live-event-stream-sse"},{"title":"Request cancellation","deprecated":null,"version":null,"openapi":"/openapi-v2.yaml post /api/v2/jobs/{id}/cancel","href":"/es/api-reference/v2/jobs/request-cancellation"}]}]}]},{"group":"v1 Cloud API (deprecated)","pages":[{"title":"Descripción general de la API de Cloud v1","sidebarTitle":"Descripción general","href":"/es/development/cloud/overview"},{"title":"Referencia de la API de Cloud","sidebarTitle":"Ejemplos de código","icon":"book-open","href":"/es/development/cloud/api-reference"},{"title":"Especificación OpenAPI","icon":"file-lines","openapi":"/openapi-cloud.yaml","href":"/es/development/cloud/openapi"},{"group":"Referencia de la API de Cloud","pages":[{"group":"workflow","pages":[{"title":"Get information about current prompt executions","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/prompt","href":"/es/api-reference/cloud/workflow/get-information-about-current-prompt-executions"},{"title":"Submit a workflow for execution","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/prompt","href":"/es/api-reference/cloud/workflow/submit-a-workflow-for-execution"},{"title":"Get available workflow templates","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/workflow_templates","href":"/es/api-reference/cloud/workflow/get-available-workflow-templates"},{"title":"Get available subgraph blueprints","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/global_subgraphs","href":"/es/api-reference/cloud/workflow/get-available-subgraph-blueprints"},{"title":"Get a specific subgraph blueprint","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/global_subgraphs/{id}","href":"/es/api-reference/cloud/workflow/get-a-specific-subgraph-blueprint"}]},{"group":"node","pages":[{"title":"Get all node information","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/object_info","href":"/es/api-reference/cloud/node/get-all-node-information"},{"title":"Get server feature flags","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/features","href":"/es/api-reference/cloud/node/get-server-feature-flags"}]},{"group":"model","pages":[{"title":"Get available model folders","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/experiment/models","href":"/es/api-reference/cloud/model/get-available-model-folders"},{"title":"Get models in a specific folder","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/experiment/models/{folder}","href":"/es/api-reference/cloud/model/get-models-in-a-specific-folder"},{"title":"Get model preview image","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/experiment/models/preview/{folder}/{path_index}/{filename}","href":"/es/api-reference/cloud/model/get-model-preview-image"}]},{"group":"job","pages":[{"title":"Manage execution history","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/history","href":"/es/api-reference/cloud/job/manage-execution-history"},{"title":"Get execution history (v2)","deprecated":true,"version":null,"openapi":"/openapi-cloud.yaml get /api/history_v2","href":"/es/api-reference/cloud/job/get-execution-history-v2"},{"title":"Get history for specific prompt","deprecated":true,"version":null,"openapi":"/openapi-cloud.yaml get /api/history_v2/{prompt_id}","href":"/es/api-reference/cloud/job/get-history-for-specific-prompt"},{"title":"List jobs with pagination and filtering","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/jobs","href":"/es/api-reference/cloud/job/list-jobs-with-pagination-and-filtering"},{"title":"Get full job details","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/jobs/{job_id}","href":"/es/api-reference/cloud/job/get-full-job-details"},{"title":"Get queue information","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/queue","href":"/es/api-reference/cloud/job/get-queue-information"},{"title":"Manage queue operations","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/queue","href":"/es/api-reference/cloud/job/manage-queue-operations"},{"title":"Interrupt currently running jobs","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/interrupt","href":"/es/api-reference/cloud/job/interrupt-currently-running-jobs"},{"title":"Get job status","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/job/{job_id}/status","href":"/es/api-reference/cloud/job/get-job-status"}]},{"group":"file","pages":[{"title":"View a file","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/view","href":"/es/api-reference/cloud/file/view-a-file"},{"title":"Get related mask layer files","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/files/mask-layers","href":"/es/api-reference/cloud/file/get-related-mask-layer-files"},{"title":"Upload an image file","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/upload/image","href":"/es/api-reference/cloud/file/upload-an-image-file"},{"title":"Upload a mask image","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/upload/mask","href":"/es/api-reference/cloud/file/upload-a-mask-image"}]},{"group":"asset","pages":[{"title":"List user assets","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/assets","href":"/es/api-reference/cloud/asset/list-user-assets"},{"title":"Upload a new asset","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/assets","href":"/es/api-reference/cloud/asset/upload-a-new-asset"},{"title":"Create asset reference from existing hash","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/assets/from-hash","href":"/es/api-reference/cloud/asset/create-asset-reference-from-existing-hash"},{"title":"Get asset metadata from remote URL","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/assets/remote-metadata","href":"/es/api-reference/cloud/asset/get-asset-metadata-from-remote-url"},{"title":"Initiate background download for large files","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/assets/download","href":"/es/api-reference/cloud/asset/initiate-background-download-for-large-files"},{"title":"Get asset details","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/assets/{id}","href":"/es/api-reference/cloud/asset/get-asset-details"},{"title":"Update asset metadata","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml put /api/assets/{id}","href":"/es/api-reference/cloud/asset/update-asset-metadata"},{"title":"Delete asset","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml delete /api/assets/{id}","href":"/es/api-reference/cloud/asset/delete-asset"},{"title":"Add tags to asset","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/assets/{id}/tags","href":"/es/api-reference/cloud/asset/add-tags-to-asset"},{"title":"Remove tags from asset","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml delete /api/assets/{id}/tags","href":"/es/api-reference/cloud/asset/remove-tags-from-asset"},{"title":"List all tags","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/tags","href":"/es/api-reference/cloud/asset/list-all-tags"},{"title":"Get tag histogram for filtered assets","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/assets/tags/refine","href":"/es/api-reference/cloud/asset/get-tag-histogram-for-filtered-assets"},{"title":"Check if asset exists by hash","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml head /api/assets/hash/{hash}","href":"/es/api-reference/cloud/asset/check-if-asset-exists-by-hash"}]},{"group":"user","pages":[{"title":"List user data files","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/userdata","href":"/es/api-reference/cloud/user/list-user-data-files"},{"title":"Get user data file","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/userdata/{file}","href":"/es/api-reference/cloud/user/get-user-data-file"},{"title":"Upload or update a user data file","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml post /api/userdata/{file}","href":"/es/api-reference/cloud/user/upload-or-update-a-user-data-file"},{"title":"Delete a user data file","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml delete /api/userdata/{file}","href":"/es/api-reference/cloud/user/delete-a-user-data-file"},{"title":"Get current user information","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/user","href":"/es/api-reference/cloud/user/get-current-user-information"}]},{"group":"system","pages":[{"title":"Get system statistics","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/system_stats","href":"/es/api-reference/cloud/system/get-system-statistics"}]},{"group":"billing","pages":[{"title":"Get workspace usage over time","deprecated":null,"version":null,"openapi":"/openapi-cloud.yaml get /api/billing/usage/timeseries","href":"/es/api-reference/cloud/billing/get-workspace-usage-over-time"}]}]}]}]}]}]},{"group":"Comfy CLI","pages":[{"title":"Empezar con Comfy CLI","sidebarTitle":"Empezar","href":"/es/comfy-cli/getting-started"},{"title":"Referencia de comandos de Comfy CLI","sidebarTitle":"Referencia de comandos","href":"/es/comfy-cli/reference"},{"title":"Solución de problemas de Comfy CLI: instalación y entorno","sidebarTitle":"Solución de problemas","href":"/es/comfy-cli/troubleshooting"}]},{"group":"Specifications","pages":[{"group":"Workflow JSON","pages":[{"title":"Workflow JSON","href":"/es/specs/workflow_json"},{"title":"Workflow JSON 0.4","href":"/es/specs/workflow_json_0.4"}]},{"group":"Node Definitions","pages":[{"title":"JSON de definición de nodo","href":"/es/specs/nodedef_json"},{"title":"JSON de definición de nodo 1.0","href":"/es/specs/nodedef_json_1_0"}]}]},{"group":"Nodos personalizados","pages":[{"title":"Nodos personalizados","sidebarTitle":"Descripción general","href":"/es/custom-nodes/intro"},{"group":"Crear nodos personalizados","pages":[{"title":"Tipos y arquitectura de nodos personalizados de ComfyUI","sidebarTitle":"Tipos de nodos","href":"/es/custom-nodes/overview"},{"title":"Crear nodos personalizados en ComfyUI","sidebarTitle":"Crea tu primer nodo","href":"/es/custom-nodes/walkthrough"},{"group":"Python (backend)","pages":[{"title":"Propiedades de un nodo personalizado","sidebarTitle":"Propiedades del nodo","href":"/es/custom-nodes/backend/server_overview"},{"title":"Ciclo de vida de los nodos personalizados de ComfyUI","sidebarTitle":"Ciclo de vida del nodo","href":"/es/custom-nodes/backend/lifecycle"},{"title":"Tipos de datos de nodos personalizados de ComfyUI","sidebarTitle":"Tipos de datos","href":"/es/custom-nodes/backend/datatypes"},{"title":"Imágenes, latentes y máscaras","sidebarTitle":"Imágenes, latentes y máscaras","href":"/es/custom-nodes/backend/images_and_masks"},{"title":"Entradas ocultas y flexibles","sidebarTitle":"Entradas ocultas y flexibles","href":"/es/custom-nodes/backend/more_on_inputs"},{"title":"Evaluación diferida en nodos personalizados de ComfyUI","sidebarTitle":"Evaluación diferida","href":"/es/custom-nodes/backend/lazy_evaluation"},{"title":"Expansión de nodos","href":"/es/custom-nodes/backend/expansion"},{"title":"Listas de datos y procesamiento de listas en ComfyUI","sidebarTitle":"Listas y procesamiento de listas","href":"/es/custom-nodes/backend/lists"},{"title":"Trabajar con torch.Tensor","sidebarTitle":"Tensores","href":"/es/custom-nodes/backend/tensors"},{"title":"Fragmentos de Python para nodos de ComfyUI","sidebarTitle":"Fragmentos Python","href":"/es/custom-nodes/backend/snippets"},{"title":"Guía de inversión del modelo de ejecución","sidebarTitle":"Modelo de ejecución","icon":"code-branch","href":"/es/development/comfyui-server/execution_model_inversion_guide"}]},{"group":"JavaScript (UI)","pages":[{"title":"Extensiones de JavaScript","sidebarTitle":"Extensiones de JavaScript","href":"/es/custom-nodes/js/javascript_overview"},{"title":"Ganchos de JavaScript de ComfyUI","sidebarTitle":"Ganchos","href":"/es/custom-nodes/js/javascript_hooks"},{"title":"Objetos de Comfy","href":"/es/custom-nodes/js/javascript_objects_and_hijacking"},{"title":"API de configuraciones","href":"/es/custom-nodes/js/javascript_settings"},{"title":"API de diálogos de ComfyUI","sidebarTitle":"API de diálogos","href":"/es/custom-nodes/js/javascript_dialog"},{"title":"API de Toast de ComfyUI","sidebarTitle":"API de Toast","href":"/es/custom-nodes/js/javascript_toast"},{"title":"Insignias del panel Acerca de","href":"/es/custom-nodes/js/javascript_about_panel_badges"},{"title":"Pestañas del panel inferior","href":"/es/custom-nodes/js/javascript_bottom_panel_tabs"},{"title":"Pestañas de la barra lateral","href":"/es/custom-nodes/js/javascript_sidebar_tabs"},{"title":"Cuadro de herramientas de selección","href":"/es/custom-nodes/js/javascript_selection_toolbox"},{"title":"Comandos y atajos de teclado","href":"/es/custom-nodes/js/javascript_commands_keybindings"},{"title":"API del menú de la barra superior de ComfyUI","sidebarTitle":"API del menú de la barra superior","href":"/es/custom-nodes/js/javascript_topbar_menu"},{"title":"Subgrafos de ComfyUI","sidebarTitle":"Subgrafos","href":"/es/custom-nodes/js/subgraphs"},{"title":"API de puntero del lienzo para widgets y cursores","sidebarTitle":"API de puntero del lienzo","href":"/es/custom-nodes/js/canvas-pointer-api"},{"title":"Ejemplos de extensiones JavaScript de ComfyUI","sidebarTitle":"Ejemplos JavaScript","href":"/es/custom-nodes/js/javascript_examples"}]},{"title":"i18n de nodos personalizados de ComfyUI: traducir definiciones","sidebarTitle":"Soporte i18n","href":"/es/custom-nodes/i18n"},{"group":"Migration Guides","pages":[{"title":"Migración a V3","href":"/es/custom-nodes/v3_migration"},{"title":"Migrar a la API del menú contextual en ComfyUI: lienzo y nodos","sidebarTitle":"Migración del menú contextual","href":"/es/custom-nodes/js/context-menu-migration"},{"title":"Migración de ID de nodo para extensiones del frontend","sidebarTitle":"Migración de ID de nodo","href":"/es/custom-nodes/js/node-id-migration"},{"title":"Migración del registro de enlaces para extensiones del frontend","sidebarTitle":"Registro de enlaces","href":"/es/custom-nodes/js/link-registration-migration"},{"title":"Migración del sistema de widgets para extensiones del frontend","sidebarTitle":"Migración del sistema de widgets","href":"/es/custom-nodes/js/widgets-migration"},{"title":"Migración de callbacks de conexión para extensiones del frontend","sidebarTitle":"Callbacks de conexión","href":"/es/custom-nodes/js/connection-callbacks-migration"},{"title":"Migración de callbacks de serialización para extensiones del frontend","sidebarTitle":"Callbacks de serialización","href":"/es/custom-nodes/js/serialization-callbacks-migration"},{"title":"Migración de geometría de nodos para extensiones del frontend","sidebarTitle":"Migración de geometría de nodos","href":"/es/custom-nodes/js/node-geometry-migration"},{"title":"Migración tras eliminar PrimeVue en extensiones del frontend","sidebarTitle":"Eliminación de PrimeVue","href":"/es/custom-nodes/js/primevue-removal-migration"},{"title":"Reemplazo de nodos","href":"/es/custom-nodes/backend/node-replacement"}]}]},{"group":"Package \u0026 Publish","pages":[{"title":"Añadir documentación de nodos para tu nodo personalizado de ComfyUI","sidebarTitle":"Documentación de nodos","href":"/es/custom-nodes/help_page"},{"title":"Plantillas de flujos de trabajo","href":"/es/custom-nodes/workflow_templates"},{"title":"Planos de subgrafos","href":"/es/custom-nodes/subgraph_blueprints"},{"title":"Comfy Registry: publica y versiona nodos personalizados","sidebarTitle":"Descripción general del Registry","href":"/es/registry/overview"},{"title":"Publicar nodos personalizados en Comfy Registry","sidebarTitle":"Publicar nodos","href":"/es/registry/publishing"},{"title":"Publicar en ComfyUI Manager","sidebarTitle":"Publicar en Manager","href":"/es/custom-nodes/backend/manager"},{"title":"Reclamar un nodo personalizado en el Comfy Registry","sidebarTitle":"Reclamar nodo","href":"/es/registry/claim-my-node"},{"title":"Estándares de calidad y seguridad de nodos del Registro de Comfy","sidebarTitle":"Calidad y seguridad","href":"/es/registry/standards"},{"title":"CI/CD de nodos personalizados","sidebarTitle":"CI/CD","href":"/es/registry/cicd"},{"title":"pyproject.toml","href":"/es/registry/specifications"},{"title":"Descripción general de la API","href":"/es/registry/api-reference/overview"},{"group":"Registry API Reference","pages":[{"group":"Admin","pages":[{"title":"Generate a short-lived JWT admin token","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /admin/generate-token","href":"/es/registry/api-reference/admin/generate-a-short-lived-jwt-admin-token"}]},{"group":"Registry","pages":[{"title":"Retrieve multiple node versions in a single request","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /bulk/nodes/versions","href":"/es/registry/api-reference/registry/retrieve-multiple-node-versions-in-a-single-request"},{"title":"list all comfy-nodes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /comfy-nodes","href":"/es/registry/api-reference/registry/list-all-comfy-nodes"},{"title":"Retrieve a node by ComfyUI node name","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /comfy-nodes/{comfyNodeName}/node","href":"/es/registry/api-reference/registry/retrieve-a-node-by-comfyui-node-name"},{"title":"Get server feature flags","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /features","href":"/es/registry/api-reference/registry/get-server-feature-flags"},{"title":"Retrieves a list of nodes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes","href":"/es/registry/api-reference/registry/retrieves-a-list-of-nodes"},{"title":"Retrieves a list of nodes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/search","href":"/es/registry/api-reference/registry/retrieves-a-list-of-nodes-1"},{"title":"Retrieve a specific node by ID","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}","href":"/es/registry/api-reference/registry/retrieve-a-specific-node-by-id"},{"title":"Returns a node version to be installed.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}/install","href":"/es/registry/api-reference/registry/returns-a-node-version-to-be-installed"},{"title":"Add review to a specific version of a node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /nodes/{nodeId}/reviews","href":"/es/registry/api-reference/registry/add-review-to-a-specific-version-of-a-node"},{"title":"Create Node Translations","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /nodes/{nodeId}/translations","href":"/es/registry/api-reference/registry/create-node-translations"},{"title":"List all versions of a node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}/versions","href":"/es/registry/api-reference/registry/list-all-versions-of-a-node"},{"title":"Retrieve a specific version of a node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}/versions/{versionId}","href":"/es/registry/api-reference/registry/retrieve-a-specific-version-of-a-node"},{"title":"list comfy-nodes for node version","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}/versions/{version}/comfy-nodes","href":"/es/registry/api-reference/registry/list-comfy-nodes-for-node-version"},{"title":"create comfy-nodes for certain node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /nodes/{nodeId}/versions/{version}/comfy-nodes","href":"/es/registry/api-reference/registry/create-comfy-nodes-for-certain-node"},{"title":"get specify comfy-node based on its id","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /nodes/{nodeId}/versions/{version}/comfy-nodes/{comfyNodeName}","href":"/es/registry/api-reference/registry/get-specify-comfy-node-based-on-its-id"},{"title":"Update a specific comfy-node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi put /nodes/{nodeId}/versions/{version}/comfy-nodes/{comfyNodeName}","href":"/es/registry/api-reference/registry/update-a-specific-comfy-node"},{"title":"Retrieve all publishers","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers","href":"/es/registry/api-reference/registry/retrieve-all-publishers"},{"title":"Create a new publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /publishers","href":"/es/registry/api-reference/registry/create-a-new-publisher"},{"title":"Validate if a publisher username is available","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/validate","href":"/es/registry/api-reference/registry/validate-if-a-publisher-username-is-available"},{"title":"Retrieve a publisher by ID","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/{publisherId}","href":"/es/registry/api-reference/registry/retrieve-a-publisher-by-id"},{"title":"Update a publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi put /publishers/{publisherId}","href":"/es/registry/api-reference/registry/update-a-publisher"},{"title":"Delete a publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi delete /publishers/{publisherId}","href":"/es/registry/api-reference/registry/delete-a-publisher"},{"title":"Retrieve all nodes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/{publisherId}/nodes","href":"/es/registry/api-reference/registry/retrieve-all-nodes"},{"title":"Create a new custom node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /publishers/{publisherId}/nodes","href":"/es/registry/api-reference/registry/create-a-new-custom-node"},{"title":"Retrieve all nodes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/{publisherId}/nodes/v2","href":"/es/registry/api-reference/registry/retrieve-all-nodes-1"},{"title":"Update a specific node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi put /publishers/{publisherId}/nodes/{nodeId}","href":"/es/registry/api-reference/registry/update-a-specific-node"},{"title":"Delete a specific node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi delete /publishers/{publisherId}/nodes/{nodeId}","href":"/es/registry/api-reference/registry/delete-a-specific-node"},{"title":"Claim nodeId into publisherId for the authenticated publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /publishers/{publisherId}/nodes/{nodeId}/claim-my-node","href":"/es/registry/api-reference/registry/claim-nodeid-into-publisherid-for-the-authenticated-publisher"},{"title":"Retrieve permissions the user has for a given publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/{publisherId}/nodes/{nodeId}/permissions","href":"/es/registry/api-reference/registry/retrieve-permissions-the-user-has-for-a-given-publisher"},{"title":"Publish a new version of a node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /publishers/{publisherId}/nodes/{nodeId}/versions","href":"/es/registry/api-reference/registry/publish-a-new-version-of-a-node"},{"title":"Update changelog and deprecation status of a node version","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi put /publishers/{publisherId}/nodes/{nodeId}/versions/{versionId}","href":"/es/registry/api-reference/registry/update-changelog-and-deprecation-status-of-a-node-version"},{"title":"Unpublish (delete) a specific version of a node","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi delete /publishers/{publisherId}/nodes/{nodeId}/versions/{versionId}","href":"/es/registry/api-reference/registry/unpublish-delete-a-specific-version-of-a-node"},{"title":"Retrieve permissions the user has for a given publisher","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /publishers/{publisherId}/permissions","href":"/es/registry/api-reference/registry/retrieve-permissions-the-user-has-for-a-given-publisher-1"},{"title":"Create a new personal access token","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /publishers/{publisherId}/tokens","href":"/es/registry/api-reference/registry/create-a-new-personal-access-token"},{"title":"Delete a specific personal access token","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi delete /publishers/{publisherId}/tokens/{tokenId}","href":"/es/registry/api-reference/registry/delete-a-specific-personal-access-token"},{"title":"Get information about the calling user.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /users","href":"/es/registry/api-reference/registry/get-information-about-the-calling-user"},{"title":"Retrieve all publishers for a given user","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /users/publishers/","href":"/es/registry/api-reference/registry/retrieve-all-publishers-for-a-given-user"},{"title":"List all node versions given some filters.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /versions","href":"/es/registry/api-reference/registry/list-all-node-versions-given-some-filters"}]},{"group":"Freepik","pages":[{"title":"Relight an image","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/image-relight","href":"/es/registry/api-reference/freepik/relight-an-image"},{"title":"Get the status of the relight task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/freepik/v1/ai/image-relight/{task_id}","href":"/es/registry/api-reference/freepik/get-the-status-of-the-relight-task"},{"title":"Style transfer an image","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/image-style-transfer","href":"/es/registry/api-reference/freepik/style-transfer-an-image"},{"title":"Get the status of the style transfer task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/freepik/v1/ai/image-style-transfer/{task_id}","href":"/es/registry/api-reference/freepik/get-the-status-of-the-style-transfer-task"},{"title":"Upscale an image with Magnific","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/image-upscaler","href":"/es/registry/api-reference/freepik/upscale-an-image-with-magnific"},{"title":"Upscale an image with Precision V2","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/image-upscaler-precision-v2","href":"/es/registry/api-reference/freepik/upscale-an-image-with-precision-v2"},{"title":"Get the status of the Precision V2 upscaling task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/freepik/v1/ai/image-upscaler-precision-v2/{task_id}","href":"/es/registry/api-reference/freepik/get-the-status-of-the-precision-v2-upscaling-task"},{"title":"Get the status of the upscaling task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/freepik/v1/ai/image-upscaler/{task_id}","href":"/es/registry/api-reference/freepik/get-the-status-of-the-upscaling-task"},{"title":"Skin enhancer using AI (Creative)","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/skin-enhancer/creative","href":"/es/registry/api-reference/freepik/skin-enhancer-using-ai-creative"},{"title":"Skin enhancer using AI (Faithful)","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/skin-enhancer/faithful","href":"/es/registry/api-reference/freepik/skin-enhancer-using-ai-faithful"},{"title":"Skin enhancer using AI (Flexible)","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/freepik/v1/ai/skin-enhancer/flexible","href":"/es/registry/api-reference/freepik/skin-enhancer-using-ai-flexible"},{"title":"Get the status of one skin enhancer task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/freepik/v1/ai/skin-enhancer/{task_id}","href":"/es/registry/api-reference/freepik/get-the-status-of-one-skin-enhancer-task"}]},{"group":"Sonilo","pages":[{"title":"Generate music from text prompt","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/sonilo/t2m/generate","href":"/es/registry/api-reference/sonilo/generate-music-from-text-prompt"},{"title":"Get a Sonilo task","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/sonilo/v1/tasks/{task_id}","href":"/es/registry/api-reference/sonilo/get-a-sonilo-task"},{"title":"Generate a song with lyrics from a text prompt","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/sonilo/v1/text-to-music-l","href":"/es/registry/api-reference/sonilo/generate-a-song-with-lyrics-from-a-text-prompt"},{"title":"Generate music from video","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/sonilo/v2m/generate","href":"/es/registry/api-reference/sonilo/generate-music-from-video"}]},{"group":"API Nodes","pages":[{"title":"Post proxyviduextend","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/extend","href":"/es/registry/api-reference/api-nodes/post-proxyviduextend"},{"title":"Post proxyviduimg2video","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/img2video","href":"/es/registry/api-reference/api-nodes/post-proxyviduimg2video"},{"title":"Post proxyvidumultiframe","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/multiframe","href":"/es/registry/api-reference/api-nodes/post-proxyvidumultiframe"},{"title":"Post proxyvidureference2video","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/reference2video","href":"/es/registry/api-reference/api-nodes/post-proxyvidureference2video"},{"title":"Post proxyvidustart end2video","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/start-end2video","href":"/es/registry/api-reference/api-nodes/post-proxyvidustart-end2video"},{"title":"Get proxyvidutasks creations","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /proxy/vidu/tasks/{id}/creations","href":"/es/registry/api-reference/api-nodes/get-proxyvidutasks-creations"},{"title":"Post proxyvidutext2video","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /proxy/vidu/text2video","href":"/es/registry/api-reference/api-nodes/post-proxyvidutext2video"}]},{"group":"Releases","pages":[{"title":"Get release notes","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /releases","href":"/es/registry/api-reference/releases/get-release-notes"}]},{"group":"Comfy Router","pages":[{"title":"List the models Comfy Router can run.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /v2/models","href":"/es/registry/api-reference/comfy-router/list-the-models-comfy-router-can-run"},{"title":"Read one partner model's catalog entry by canonical model ID.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /v2/models/{provider}/{model}","href":"/es/registry/api-reference/comfy-router/read-one-partner-models-catalog-entry-by-canonical-model-id"},{"title":"Run a partner model synchronously by canonical model ID.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /v2/models/{provider}/{model}","href":"/es/registry/api-reference/comfy-router/run-a-partner-model-synchronously-by-canonical-model-id"},{"title":"Price a model call before running it, without running it.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /v2/models/{provider}/{model}/estimate","href":"/es/registry/api-reference/comfy-router/price-a-model-call-before-running-it-without-running-it"},{"title":"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.\nThe INPUT schema served is the SAME source the server validates a call against before the request reaches the provider. That is the property that makes it worth trusting: what Comfy publishes and what Comfy enforces are one document, not two copies that drift. Both read the schema through a single accessor (`routerschema.Source.InputSchema`), so they cannot diverge without deleting it.\nThe path prefix is the SAME canonical `{provider}/{model}` model ID that `POST /v2/models/{provider}/{model}` invokes, with `/openapi.json` appended - a caller reads an ID out of the catalog, appends one literal segment, and gets that model's schema. `provider` and `model` are governed by the same `RouterProvider` / `RouterModel` parameters, with the same alphabet.\nOne constraint this puts on the still-unsettled `{variant}` segment: `openapi.json` is itself a legal value under the `RouterModel` alphabet, so whatever spelling addresses a variant must not make `/v2/models/{provider}/{model}/openapi.json` ambiguous with a variant literally named `openapi.json`. Reserving that one literal is the cheapest resolution, and it is noted here rather than resolved, because how the variant segment is addressed is not settled by this contract.\nBOTH halves are described: the request body under the operation's `requestBody`, and the response body under its `200`. They do NOT promise the same thing, and the served document says which is which in its own `info.description`. The INPUT schema is ENFORCED - it is the schema the server validates against - so what is published and what is enforced cannot differ. The OUTPUT schema is DESCRIPTIVE: Router returns the provider's native result document exactly as it arrived, and never validates, narrows or re-envelopes it, so the output schema describes what the provider sends rather than constraining it. Comfy owns no output shape of its own.\nA model whose OUTPUT Comfy has not described yet is served a permissive output alongside whatever its input half is, flagged `x-comfy-output-schema-authored: false`. The two authored flags are independent: an authored input with an undescribed output is the ordinary state, and a caller must read each flag rather than either one for both.\nA model whose schema has not been authored yet is served a MINIMAL PERMISSIVE document with `200`, NOT a `404`: the model exists, `GET /v2/models/{provider}/{model}` reports it and `POST` runs it, so 404 here would have two Router routes disagreeing about whether the same model exists. The permissive document says the true thing instead - this model takes a JSON object and Comfy has not yet narrowed which fields - and flags itself with `x-comfy-input-schema-authored: false` so a caller can tell \"unconstrained\" from \"constrained to an open object\". A `404` on this route means only what it means everywhere else in Router: `model_not_found`, the ID names nothing.\nThe response is cacheable. It carries a strong `ETag` over the document bytes and honours `If-None-Match` with a `304`, because an SDK re-fetches this document far more often than the document changes.\nThis operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the invocation route.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /v2/models/{provider}/{model}/openapi.json","href":"/es/registry/api-reference/comfy-router/read-one-partner-models-input-and-output-schemas-as-an-openapi-document"},{"title":"Submit a partner model run to the queue and return immediately.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi post /v2/models/{provider}/{model}/requests","href":"/es/registry/api-reference/comfy-router/submit-a-partner-model-run-to-the-queue-and-return-immediately"},{"title":"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.\nWHILE 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.\nA 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.\nWITH 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.\nA 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.\nThis operation is deliberately tagged `Comfy Router` and NOT `API Nodes`, for the reason given on the synchronous route.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /v2/models/{provider}/{model}/requests/{request_id}","href":"/es/registry/api-reference/comfy-router/collect-the-result-of-one-submitted-request"},{"title":"Ask for one submitted request to be cancelled.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi put /v2/models/{provider}/{model}/requests/{request_id}/cancel","href":"/es/registry/api-reference/comfy-router/ask-for-one-submitted-request-to-be-cancelled"},{"title":"Read the queue state of one submitted request.","deprecated":null,"version":null,"openapi":"https://api.comfy.org/openapi get /v2/models/{provider}/{model}/requests/{request_id}/status","href":"/es/registry/api-reference/comfy-router/read-the-queue-state-of-one-submitted-request"}]}]}]}]}]},{"tab":"Soporte","pages":[{"title":"Soporte","href":"/es/support/contact-support"}]}]}]},"basePath":"","title":"Submit a partner model run to the queue and return immediately. - ComfyUI","buildId":"dpl_Fyv1NbzwP68g2bEH2Roy98uY1zmE","deploymentId":"dpl_Fyv1NbzwP68g2bEH2Roy98uY1zmE","dynamics":["c2aa737661","2b790eda92","b0f84f53ff","a9bb46feb3"],"chunks":["OpenApiReferenceProvider-DdEMm2kG.js","utils-BMo572L7.js","cloneDeep-izs3Hzxb.js","_Uint8Array-BVIqJ90l.js","isObjectLike-C8vmzPEN.js","isArray-Dcu1dQ5t.js","utils-C-4m9KAx.js","toString-BX-iyh8G.js","isSymbol-CSH9U2Gm.js","addUniqueKeysToSchema-DPLK2Oq9.js","snippetPresets-BKjfJMC9.js","style-to-object-BY3JEAdk.js","v4-DIgz8zy6.js","conditional-requirements-CFM0id0Q.js","ApiExamples-DTsgtCEj.js","isEqual-BfaiBbcl.js","usePlaygroundInputsStore-C8vmhyQM.js","shallow-r9w8LQGS.js","getApiPlaygroundDisplay-DjPI64HQ.js","CodeSnippet-C8Bvhl16.js","CodeSnippets-BtWUwcMy.js","Page-BIYvYBgk.js","getPastedJson-Dcm_3Pgl.js","icon-BXwrTpjc.js","MethodPill-CcjfVmmy.js","DuplicateIcon-BuKIbXAQ.js","createIcon-DaL9yXsO.js","DynamicMarkdown-Co5nikxh.js","SectionHeading-P4UNTsMZ.js","axios-vUo_bRtp.js","Markdown-CEfymfIH.js","clsx-Dq9TQuNi.js","lib-DuNMoZ05.js","lib-B8HbPgcC.js","html-void-elements-mCSPHezU.js","dist-D2J8knkn.js","katex-YMM-FMeD.js","marked.esm-CZWythHk.js","AssistantPageLink-BgZ2ThuU.js"]}