Comfy Router is not generally available yet. The routes referenced below —
POST /v1/models/{provider}/{model} and its catalog and schema siblings — are
not serving requests yet: an authenticated call answers 404 today. This page
describes the contract they will serve, published ahead of that rollout so an
integration can be written against a known shape. Everything below is a
statement about that contract, not about behaviour you can exercise right now.200 carries that model’s native output. That shape is what makes the first integration short, and it is also where every limit on this page comes from. Read this page before you design around Router, not after — most of what follows has a straightforward alternative, and the ones that do not are worth knowing before you build on an assumption Router does not hold.
At a glance
Each row links to the section that explains it. Deliberate means the limit is part of how Router works and is not waiting on anything; not yet means Router is expected to gain the capability, though this page makes no commitment about when.No queued submission
There is one way to run a model:POST /v1/models/{provider}/{model}, which holds the connection until the generation finishes and returns the result in the response. There is no endpoint that accepts a job, hands you an identifier and lets you collect the result later, and no callback or webhook on completion. A queued counterpart is planned and is referenced in the API reference as /v1/queue/models/{provider}/{model}; it is not part of the contract today, and a call to it is not served.
What to do instead. For most models this is a non-issue: keep the connection open and read the result. A fast image model returns in a few seconds; a long video generation can run for minutes, and Router will hold the connection for it. Set a generous client read timeout — above Router’s own deadline — and treat the call as long-running rather than as a fast request. If your architecture genuinely cannot hold a connection open — a serverless function with a short execution ceiling, a browser tab you expect the user to close — then run the call from a worker you control that can, or use a partner-proxy route for a provider that exposes its own submit-and-poll pair. See the last section.
Status: not yet. The queued path is expected; nothing on this page commits to when.
No cost or credit figures on a response
A Router response tells you what the model produced, and its contract says nothing about what it cost. There is no charge amount, no credit balance and no usage figure in the body, and the route declares no cost header. One caveat, so it does not surprise you: Router shares a billing path with the partner-proxy routes, and that path stampsX-Comfy-Credits-Used on a billed response for an allowlist of providers, so the header can appear on a Router call to one of them. It is not part of Router’s contract — it is absent for every provider outside that allowlist, and it is deliberately not replayed on an idempotent retry, precisely so a client summing it cannot double-count a call that was only paid for once. Do not build reconciliation on it. The model catalog is the same: it carries billing facts a caller needs before invoking, never prices. So you cannot reconcile spend from a Router response alone, and you cannot show a user “this call cost X” without getting X from somewhere else.
What to do instead. Your balance, your usage and your invoices live on the Comfy platform at platform.comfy.org — that is the source of truth for what you have spent and what you have left, and it is unaffected by anything on this page. Two things Router does tell you at call time are worth using: a call refused for lack of credit comes back as insufficient_credits, so you can handle exhaustion as a typed error rather than by pre-checking a balance; and each model’s catalog entry carries billing.charges_on_policy_rejection, which says whether that specific model charges you for a generation it then refuses on content-policy grounds. It is a string with three values, not a boolean: yes, no and unknown. Read unknown as “this might charge you” — it means nobody has established that model’s behaviour yet, and it exists precisely so an unchecked model is not published as a no, which is a claim. The field is deliberately not an enum, so treat any value you do not recognize as unknown too, and do not write a truthiness check over it: the string "no" is truthy in most languages, and that check gets backwards the one case it exists to catch. Providers differ on that, the difference is invisible at call time, and reading it before you call is how you avoid a charge you cannot explain afterwards.
Status: not yet for per-call figures. Note that the catalog deliberately carries no prices — pricing belongs where pricing is maintained, not duplicated into a model listing that would drift from it.
No way to resume a call you lost
Router does not keep a resumable record of an in-flight call. There is no status route, no job identifier, and nothing to reconnect to: if the connection drops mid-call — a client crash, a network partition, a deploy that restarts your process — the response is gone, and the call is not something you can ask about afterwards. Whether the generation completed and was charged is a separate question from whether you received it, and losing the connection does not reliably answer either. What to do instead. Send anIdempotency-Key header on every call. It does not make a lost call resumable, but it makes retrying one safe. Router reserves the key for the duration of the call, and when the call actually reached you with an answer it records that response against the key for 24 hours; retrying with the same key then replays the recorded response instead of dispatching — and re-charging — the provider a second time, marked Idempotent-Replayed: true so you can tell a replay from a fresh run. Generate a fresh key per logical call, not per attempt; the same key presented with a different request body is a 409 rather than a silent overwrite.
Be precise about what that buys you, because it is a billing property and not a delivery one: a key is charged at most once. It is not a promise that a key is dispatched to the provider at most once. Router holds a key against an answer you actually received; the outcomes that charged you nothing release it so the call can be made again. A 5xx, a 408/425/429, and — this is the one that matters here — a call where nothing reached you at all: each of those releases the key, and a retry with it genuinely re-runs and re-dispatches the provider.
So a dropped connection is the case idempotency does not rescue. A connection lost mid-call usually means no response was ever committed to you, which is exactly the release path above: retrying with the same key starts a fresh run rather than handing you the result you missed, and if the original generation had already been dispatched the provider may run it a second time. That is the right default — an unbilled call you never received should be re-runnable — but plan for “retry produces a new run”, not “retry collects the lost one”.
When Router does hold something for the key, the retry is answered rather than re-run: either the original response replayed, or a 409 explaining why it cannot be. A retry sent while the original is still in flight is a 409 carrying Retry-After, so wait and re-send the same key. A retry against a call that completed but whose response Router could not keep a faithful copy of is also a 409 — and that is not only the oversized-response case: a response past the replay cap, a handler that failed or panicked after answering, and a write to you that failed or came up short all record the key as consumed-but-not-replayable and return the same 409. Do not go hunting for a size problem when you see it. The guidance in every one of those cases is the same: use a new key. The original completed and was charged, and Router will neither invent its response nor re-run it under the old key.
Not yet in the generated contract. The
Idempotency-Key request header, the 409 response and the Idempotent-Replayed and Retry-After response headers described here are not declared on POST /v1/models/{provider}/{model} in the OpenAPI contract the reference is generated from, so they do not appear in the generated API reference and the SDKs do not model them. Send and read them yourself until they do.Calls are cut off at a server deadline
One Router call may hold its connection for 10 minutes. That is the default; it is a server-side configuration value rather than a fixed constant, so treat it as the number to design against rather than a guarantee etched into the contract. Past it, Router stops waiting, cancels its own in-flight request to the provider and answers504 with X-Comfy-Error-Type: deadline_exceeded. A deadline_exceeded call is not billed — the bound is ours, so its cost is ours.
Two things that cancellation does not do, both worth knowing before you retry. It does not recall a generation a provider has already accepted: for the partners Router drives by submitting a job and polling it, expiring the deadline ends Router’s own wait, not the provider’s work, so that job can run to completion and a retry can produce a second generation (you are still not billed for the timed-out call). And it cannot un-send an answer: if the handler wins the race and commits a response just as the bound expires, you keep that response rather than the 504.
Do not confuse it with the other 504. provider_timeout is the partner failing to answer in time, and that one is billed; deadline_exceeded is Router’s own bound expiring. Two causes, two billing outcomes, which is exactly why they are two buckets on the same status code — branch on X-Comfy-Error-Type, never on the status alone.
What to do instead. Set your client’s read timeout comfortably above the deadline, not below it. A client that gives up first turns a typed 504 with a request identifier into an opaque local abort, and you lose the one artifact support can trace. If a single generation genuinely cannot finish inside the deadline, Router is not the right shape for it today: run it through a partner-proxy route that submits and polls, or break the work into calls that each finish inside the bound.
Status: deliberate. A bound has to exist — without one, a stuck upstream holds a connection and a concurrency slot indefinitely. The specific number may be tuned; the existence of a deadline will not go away.
No progress while a call runs
POST /v1/models/{provider}/{model} returns exactly once, at the end. There is no streaming response, no server-sent events, no percentage, no partial or preview frame. This holds even for partners whose own API is submit-and-poll: Router does that polling internally, inside your one call, and the intermediate states it sees are not forwarded to you. From the outside, a three-second image and a six-minute video are the same shape — one request, one response, nothing in between.
What to do instead. On Router today, nothing: show an indeterminate progress state rather than a percentage you cannot source. If progress is a hard requirement for a specific provider, check whether that provider’s partner-proxy routes expose their own polling or streaming and use those directly — a few do, and they are unchanged and fully supported.
Status: not yet, and tied to the queued path: progress needs somewhere to report to, which a queued submission provides and a single synchronous call does not.
Three forecast buckets are not in the vocabulary
Router’serror_type vocabulary is a closed set of fourteen buckets — the fourteen the API reference lists and the quickstart points at. Three more are named in that reference’s prose as expected additions: file_download_error, cancelled and queue_timeout. They are named, and that is all they are. They are not members of the set today: no Router response carries one, a client generated from the contract does not know them, and if Router were handed one internally it substitutes internal_error rather than putting it on the wire. So a branch you write for them today is a branch that never runs, and their appearance in the reference is not evidence that Router cancels calls or queues them — it does neither.
They are forecast in writing rather than left out entirely because error_type is deliberately a plain string and not an enum, and a client that hard-rejects an unrecognized bucket fails hardest exactly when something has already gone wrong. Naming the additions in advance is how a reader knows the set is open-ended by design.
What to do instead. Handle the fourteen buckets Router actually publishes, listed in full in the API reference, and write one fallback branch that treats any unrecognized value as internal_error. That fallback is the whole mechanism: it is what lets these three, and any bucket added after your client was written, arrive without breaking you. Branch on the coarse bucket for control flow, and read the per-field type inside a 422 body when you need the specific reason.
Status: not yet. Each of the three corresponds to behaviour Router does not have yet, and each joins the vocabulary in the same change that starts emitting it — never before.
Router does not cover every partner operation
Router runs partner models. It does not front every operation a partner exposes — the file uploads, the account and asset reads, the provider-specific management calls, the streaming chat endpoints and the submit-and-poll pairs that some partners publish. Nor does Router reshape any of them: it forwards a model’s native input and returns its native output unchanged, so there is no unified envelope to port an unsupported operation onto. What to do instead. The partner-proxy routes under/proxy/… remain fully supported on the same host, with the same credential, and they are the answer for anything Router does not cover. They are not deprecated, they are not on a sunset path, and using them alongside Router in the same integration is expected rather than a workaround. Reach for Router when you want one route shape and one credential across many models; reach for /proxy/… when you need a specific partner operation, a provider’s own streaming response, or the submit-and-poll control that Router deliberately hides.
Status: deliberate. Router narrows the surface on purpose — one route shape is the feature. The proxy surface stays where it is.
Next
- Comfy Router quickstart — a first working call in Python or TypeScript.
- Comfy Router API reference — every endpoint, every parameter and every error bucket Router does send.