curl --request POST \
--url https://api.comfy.org/v2/models/{provider}/{model}/estimate \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://api.comfy.org/v2/models/{provider}/{model}/estimate"
payload = {}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({})
};
fetch('https://api.comfy.org/v2/models/{provider}/{model}/estimate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.comfy.org/v2/models/{provider}/{model}/estimate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.comfy.org/v2/models/{provider}/{model}/estimate"
payload := strings.NewReader("{}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.comfy.org/v2/models/{provider}/{model}/estimate")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.comfy.org/v2/models/{provider}/{model}/estimate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{}"
response = http.request(request)
puts response.read_body{
"currency": "<string>",
"model": "bfl/flux-2-pro",
"pricing_as_of": "2023-11-07T05:31:56Z",
"provider": "<string>",
"source": "<string>",
"amount": "<string>",
"amount_cents": 123,
"credits": 123,
"max_amount": "<string>",
"max_amount_cents": 123,
"min_amount": "<string>",
"min_amount_cents": 123,
"reason": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "image_too_small",
"ctx": {},
"input": "<unknown>"
}
]
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}Price a model call before running it, without running it.
Returns a pre-flight estimate, in US dollars, of what running this body on this model will cost, priced at Comfy’s standard list price (a model billed the cost its vendor reports is estimated from the vendor’s published price list instead). It does not reflect contract or customer-specific rates: a workspace with negotiated pricing is quoted the list price and charged its contract rate, and source: exact then means exact at list price, not that workspace’s charge. It is an estimate, not the final charge: the settled charge in your workspace’s usage and billing records is what you pay. The body is validated against the model’s input schema first, as a default run (no strict_mode, no reject_unknown_fields) validates it, so a body that fails the model’s schema is the same 422 here as on the run; nothing is dispatched to a provider and nothing is charged. The answer is not a price lock: it is priced at the rates in force at pricing_as_of, and a run is charged at the rates in force when it is rated. If estimates are switched off for a deployment the route answers 403 with error_type: not_enabled; a model Router cannot run yet answers the same 403 with the run route’s own detail, so read detail to tell the two apart. The body is priced as Router would dispatch it: translated for the model_provider leg it names and retargeted to the operation it runs on, in the same order as the estimate on the queued submit’s 201. That estimate is not a pre-flight price, though: it arrives only after the run is admitted and queued, and that run is charged unless it is cancelled before dispatch.
source says how much to trust the figure. exact carries amount; estimated carries min_amount and max_amount and no amount; unknown carries neither and a reason. Never read an absent amount as free.
The figure is the platform charge. A run Comfy does not bill because it uses your own provider key (BYOK) is not quoted as such here.
A model_provider request is quoted for the named alternate provider’s leg, with the same refusals the run route applies to it. fallback_provider and strict_mode are not accepted: a synchronous run whose fallback retry lands on another provider is charged at that provider’s own rate, which this estimate did not quote, and X-Comfy-Router-Fallback-Provider on the run’s response is how a caller detects that it happened. Idempotency-Key is ignored on this route: an estimate never reserves or replays a key, so the same key can be sent on the run that follows.
This operation is deliberately tagged Comfy Router and not API Nodes, for the reason given on the synchronous route.
curl --request POST \
--url https://api.comfy.org/v2/models/{provider}/{model}/estimate \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '{}'import requests
url = "https://api.comfy.org/v2/models/{provider}/{model}/estimate"
payload = {}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({})
};
fetch('https://api.comfy.org/v2/models/{provider}/{model}/estimate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.comfy.org/v2/models/{provider}/{model}/estimate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.comfy.org/v2/models/{provider}/{model}/estimate"
payload := strings.NewReader("{}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.comfy.org/v2/models/{provider}/{model}/estimate")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.comfy.org/v2/models/{provider}/{model}/estimate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{}"
response = http.request(request)
puts response.read_body{
"currency": "<string>",
"model": "bfl/flux-2-pro",
"pricing_as_of": "2023-11-07T05:31:56Z",
"provider": "<string>",
"source": "<string>",
"amount": "<string>",
"amount_cents": 123,
"credits": 123,
"max_amount": "<string>",
"max_amount_cents": 123,
"min_amount": "<string>",
"min_amount_cents": 123,
"reason": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": [
{
"loc": [
"<string>"
],
"msg": "<string>",
"type": "image_too_small",
"ctx": {},
"input": "<unknown>"
}
]
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}{
"detail": "<string>",
"error_type": "invalid_input",
"billed_workspace": {
"type": "<string>",
"id": "<string>",
"rail": "<string>"
},
"credential_kind": "<string>",
"denied_reason": "<string>",
"refusal_subject": "<string>",
"upstream_detail": "<string>"
}Autorizaciones
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).
Parámetros de ruta
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.
64^[a-z0-9]+([._-][a-z0-9]+)*$"bfl"
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.
128^[a-z0-9]+([._-][a-z0-9]+)*$"flux-2-pro"
Parámetros de consulta
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_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.
Cuerpo
The partner model's native JSON input, identical to the body the synchronous route accepts for this model. It is validated against the model's own input schema as a default run validates it (this route takes no strict_mode or reject_unknown_fields), and its own fields (the output count, the operation an input image selects) decide what is priced.
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
The quote. Read source before reading any amount: only exact and free carry amount (free as "0"), and an unknown quote carries a reason instead of a figure.
A pre-flight estimate of what this call will cost, in US dollars, priced before dispatch at Comfy's standard list price, or — for a model billed the cost its vendor reports — from the vendor's published price list. For that vendor-reported case, source: exact is returned only for a small opted-in set of models, and only when the published price list fully determines the figure (every price-driving field was stated, or left at a default that cannot change it); every other vendor-reported quote is estimated, including some the list would fully determine. Either way the run is still billed the amount the vendor reports after it completes, never the quoted figure verbatim. It does not reflect contract or customer-specific rates: a workspace with negotiated pricing is quoted the list price and charged its contract rate, and source: exact then means exact at list price, not that workspace's charge. It is not the final charge: the settled charge in your workspace's usage and billing records is what you pay. X-Comfy-Credits-Used, reported after the run, is a rounded post-hoc figure, not a receipt. source says how much to trust the number: exact carries amount; free carries amount "0" (the rate card prices this request at zero); estimated carries min_amount and max_amount and no amount; unknown carries neither and a reason. Never read an absent amount as free. Not a price lock: see pricing_as_of.
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.
TestRouterCatalogIdsMatchInvocationRoute 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.
193^[a-z0-9]+([._-][a-z0-9]+)*/[a-z0-9]+([._-][a-z0-9]+)*$"bfl/flux-2-pro"
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.
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.
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.
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.
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.
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.
¿Esta página le ayudó?