POST /v2/models/{provider}/{model}. The route and authentication stay the same across models.
Discover the catalog
List models with the same API key used for generation:id in the invocation path. The billing object contains billing facts, not a price. Read policy-refusal billing before relying on it.
Pagination
- When
has_moreistrue, pass the returnednext_cursorascursor. Stop whenhas_moreisfalse, even if an earlier page was shorter than requested. - Treat cursors as opaque. URL-encode the value, for example with cURL
--get --data-urlencode "cursor=$NEXT_CURSOR"; do not calculate offsets or modify the cursor. limitdefaults to 20 and is capped at 100. Values above the cap are clamped; zero and negative values select the default. The response reports the limit actually used.- An invalid cursor returns
400/invalid_input. It does not silently restart the list. - A cursor can remain valid across catalog updates, but traversal is not a snapshot: a model added before your current position may not appear in that walk.
503 / service_unavailable is temporary. Retry with backoff; do not treat it as an empty catalog. SDK run methods call the selected model directly.
Read one model
Fetch a catalog entry directly when you know the model ID:Read input and output schemas
Each model exposes a standalone OpenAPI document:requestBody describes the input, and the 200 response describes the output when an output schema has been authored. Input validation and output documentation are different: Router validates against its input schema but does not validate the returned provider result against its output schema.
Inspect the output media type as well as its fields. An unauthored output can use */*, and some models return binary data rather than JSON.
Cache a schema
Save the schema and itsETag. On a later schema fetch, pass that ETag in If-None-Match. A 304 has no body; keep the cached document. A 200 supplies a replacement document and ETag.
Cache-Control: private, must-revalidate. Keep authenticated responses out of shared caches. This ETag/304 behavior applies only to the schema endpoint.
Validation and fallback schemas
An authored input schema rejects invalid fields before the provider call with422 and a detail[] array. Read the field paths in loc; see validation errors.
Some schemas accept any JSON object and set x-comfy-input-schema-authored: false. Router forwards those requests without model-specific validation, so the provider can still reject them.
bfl/flux-2-pro currently uses this fallback. Check the provider documentation or its model page for required fields.
Read the result
Router returns each model’s terminal result shape. There is no common image, video, or text envelope: BFL image output usesresult.sample, while other models can return URL lists or inline bytes.
Some asset URLs are rehosted by Comfy; others remain provider URLs or inline bytes. Check result assets and download expiring assets promptly. Replays do not renew URLs.
Handle errors, retries, and billing
Read errors defensively
A failed request can return a proxy’s HTML error page, truncated JSON, or plain text. Do not let a JSON parsing error hide the HTTP status or request ID. These helpers use anhttpx.Response in Python and a Fetch Response in TypeScript; the SDKs already expose error fields for normal SDK calls.
Validation errors
A Router422 means validation failed before the provider call and is not billed. Its body has a detail[] array, with one entry per rejected field. The error category is in X-Comfy-Error-Type, not in the body. For example:
422.
400 describes a request-level problem, such as a malformed cursor, rather than this per-field validation body. The error reference lists the supported categories. Treat an unknown category as internal_error for control flow, but keep the original value for diagnostics. Do not hard-reject a new error value or implement forecast error categories as though they already occur.
Retry safely
Persist the key with the model ID and request body before sending. Reuse it for every attempt of that logical call. Router does not return theIdempotency-Key to you in its response. The Python SDK includes its key on raised exceptions; in TypeScript, keep your supplied key yourself.
Keys are shared within the workspace carried by the credential, or scoped to the user when it carries no workspace. Use a UUID unique across that scope and retry with the same credential. Reusing another workspace member’s key can return their recorded result or a conflict; changing credentials can start a separate, billable call.
Router retains keyed response or collection state for 24 hours; a retry does not start a new retention window. Once that state expires, do not expect the old key to recover a result or prevent a new dispatch. A key also does not make an expired asset URL usable again.
Retry outcomes
Conflicts compare the method, model path, query, and body. A key can become non-replayable after an oversized response, failed response write, or an asset that cannot be safely replayed. Waiting does not recover a consumed result. A new key starts a new call; it does not retrieve the old output.
A refusal before provider dispatch releases the key. A dispatched call can retain a provider handle or become non-replayable. Do not infer key state or billing from the status code alone.
Do not mint a brand-new key just because a call timed out or the connection dropped. If Router already accepted the generation, a new key can create a second logical run and therefore a second billable outcome. Reuse the same key until you know the original call is unrecoverable.
Timeouts and collection
One Router call may hold the connection for 10 minutes by default. Set your client timeout above that bound so you keep the typed504 and the request ID rather than an opaque local abort.
deadline_exceeded is Router’s waiting limit; provider_timeout is the provider’s deadline. A provider generation that completes can be billed even if the caller received a timeout or disconnected. Client cancellation stops the wait and SDK retries, but does not necessarily cancel accepted provider work.
For submit-and-poll providers, a retained handle lets a same-key request continue collecting the original generation. Dispatched calls cut off without a recoverable handle can consume the key without a replayable result; a same-key retry then returns 409. Provider-attributed transient failures without a captured success can still release the key for another attempt. The absence of a handle alone does not tell you which outcome applies.
The SDKs retry some failures within a bounded budget. Once they return an error, keep the request and key rather than generating a new one. For raw HTTP, this example retries only the two explicit collection hints:
Retry-After. HTTP errors retain the response for inspection; transport errors propagate without replacing the key. Schedule later collection with the saved key if your application needs a longer recovery window.
Model billing facts
GET /v2/models and the model detail response include billing.charges_on_policy_rejection. This describes a policy refusal, not every failure or a price estimate.
Compare these strings explicitly:
"no" is truthy in Python and JavaScript. Treat any unrecognized value as unknown. A request refused for lack of credits reports insufficient_credits.
Provider payloads may include their own cost or usage numbers; those are not the Comfy charge. X-Comfy-Credits-Used may appear for an allowlist of providers but is not universal and is not replayed. Use workspace usage and invoices for reconciliation. Preserve the request ID when investigating a charge.
Per-model examples
- Google Gemini
- Nano Banana 2
- Nano Banana 2 Lite
- Nano Banana Pro
- FLUX 1.1 Pro Ultra
- FLUX Kontext
- FLUX Video Upscale
- FLUX 3 Video
- Ideogram 4
Next
- Quickstart: installation, invocation, and saving the image.
- API reference: endpoint parameters, schemas, and response codes.