Skip to main content
Comfy API v2 is versioned. Changes within v2 are additive; breaking changes ship as v3. Report problems through Feedback.
One package, two surfaces. comfy-sdk (PyPI) and @comfyorg/sdk (npm) ship two clients that talk to two different services. Method names repeat across them (run, submit, events) and mean different things in each, so check which client a snippet builds before you copy it. Neither surface wraps the other, and one API key created in your Comfy workspace works for both.
The two languages expose the Router surface differently. In Python, Comfy() carries both: client.models.run(...) for Router and client.workflows / client.assets / client.jobs for Cloud. In TypeScript they are separate exports with deliberately similar names: comfy (lowercase, the module-level namespace) holds comfy.models, while Comfy (the class) is the Cloud client and has no .models.
This page documents the second row: the Comfy Cloud and Comfy API v2 client. For the model router, start at the Comfy Router quickstart. The Comfy SDKs let your application run ComfyUI workflows and get the results back. You submit a workflow, ComfyUI executes it, and you download the outputs. The same code runs against Comfy Cloud or against a ComfyUI instance you host yourself. Only the base URL changes, except for job labels, which work only on a Comfy API deployment, and the job list, which Comfy Cloud does not offer yet. The SDKs are clients for the Comfy API v2, a versioned HTTP API that we intend to support long term. New releases of ComfyUI will not break integrations built on it. Things people build this way:
  • Plugins that generate content inside another application, such as Blender or Krita
  • Consumer apps that run generation on behalf of their users
  • Batch pipelines, for example running one workflow over every frame of a video
  • Backend services that need many workflows in flight at once
These SDKs drive ComfyUI from the outside. If you are writing custom nodes or frontend extensions that run inside ComfyUI, you want Develop Custom Nodes instead. Those are a separate set of APIs.

Install

Python 3.10 or newer. Node 22 or newer.
Current release: 0.5.0. comfy-sdk on PyPI and @comfyorg/sdk on npm are released together and share a version number. Install the latest (pip install comfy-sdk, npm install @comfyorg/sdk) or state a range rather than an exact pin. The two ecosystems spell that differently: comfy-sdk>=0.5 is a floor and accepts later minors, while @comfyorg/sdk@^0.5 is npm’s compatible range for 0.5.x and stops short of 0.6.0, so a caret range needs raising when a new minor ships. Use @comfyorg/sdk@>=0.5.0 if you want the npm side to track later minors too. The registry pages are authoritative if this note is behind.

Quickstart

Upload an input image, run a workflow, and write the results to disk.
workflow_api.json is a workflow saved in API format. "10" and "9" are node IDs from that file: the node the input image feeds into, and the output node whose results you want. Asset handles are lazy. photo.png is hashed locally and only uploaded if the server does not already have those bytes, so re-running with the same input costs nothing. run() submits the job and waits for it to reach a terminal state. To do work while it executes, use submit() instead and watch the event stream. To run against your own ComfyUI instead, set COMFY_BASE_URL and drop the key. See below.

Choosing a base URL

The base URL comes from the COMFY_BASE_URL environment variable, not a constructor argument:
It is read each time a client is constructed, must be an http(s) URL, and unset or blank means Comfy Cloud. So the client itself is the same everywhere:
Upgrading from an early build? Comfy("<url>", "<key>") is now Comfy(api_key="<key>") with COMFY_BASE_URL set. api_key is keyword-only, so the old positional call raises TypeError rather than quietly reading a URL as a key.

Comfy Cloud

Works out of the box. Create an API key and pass it to the client.
API access requires a paid Comfy Cloud subscription. The free tier does not include it. How many jobs you can run at once depends on your tier. See Concurrent jobs.

Comfy API deployment

A workflow you deployed through the developer platform gets its own endpoint. Point COMFY_BASE_URL at it and use your API key, exactly as with Comfy Cloud. Everything in this guide works the same way, except job labels and the job list, which Comfy Cloud does not support yet. A Comfy API deployment can run multiple workflows with the models and custom nodes included in its Build. For each job, get_workflow() returns a workflow response with format: "api" and the executed graph in its .graph property.

Your own ComfyUI

The v2 API is served on self-hosted ComfyUI by comfy-api-proxy, a small open-source service that runs alongside your ComfyUI. Install it, run it, and set COMFY_BASE_URL="http://127.0.0.1:8189":
See API Proxy for Self-Hosted ComfyUI for configuration and authentication details.

Retrying a submit: the idempotency key

submit() and run() send an Idempotency-Key with every submission, and the Comfy API v2 contract for that key is reject-on-duplicate, not record-and-replay. Read this before you wrap a submit in a retry loop.
  • Each call mints a fresh key. Calling submit() twice with the same workflow is two submissions, two jobs and two charges. A naive for attempt in range(3) around submit() is a double-bill, not a retry.
  • A reused key is rejected, not replayed. Pass your own idempotency_key (idempotencyKey in TypeScript) to make a retry idempotent, and the second request with that key fails 422 idempotency_key_reuse instead of returning the first job. The SDKs raise IdempotencyKeyReuse. That is the opposite of what “idempotent retry” usually means, so handle it explicitly.
  • Recovery is to go and find the job, not to resubmit. On IdempotencyKeyReuse the first attempt may well have created a job. Fetch it with client.jobs.get(job_id). That lookup needs an id you already stored, so it only recovers the case where the submit returned and you persisted the id before the failure. If the connection dropped before the id was recorded there is nothing to look up and no automatic recovery: the examples below re-raise for manual handling rather than resubmitting under a claimed key.
  • The key is released when the submission definitively failed. A validation error, an out-of-credits reject or a queue-full reject creates no job and frees the key, so resubmitting under it is fine. After an ambiguous failure (a read timeout, a connection dropped mid-request) the key stays claimed: look for the job rather than resubmitting.
  • Keys expire after 24 hours and must be non-empty, printable ASCII and within the length bound. An invalid key raises ValueError before any request is made, so an explicit "" never silently falls back to a minted key.
A retry that is safe to run more than once therefore has to carry two things across attempts: the key, so the server can tell a retry from a new submission, and the job id, so you can find the job a claimed key already created.
client.jobs.get(job_id) is the SDK’s rehydration path, and it needs an id, which is why recording the id the moment a submit returns is what makes this recoverable at all. See Comfy API v2 for the full key contract on POST /api/v2/jobs.
Comfy Router’s Idempotency-Key behaves differently. On Router the key is a replay handle rather than a single-use token: re-sending a queued submit under the same key returns the original request instead of queueing a second one. See queued idempotency and billing and Router retry outcomes. The two surfaces share a header name, not a contract.

Watching a job run

job.events() gives you a live stream of the job’s state: node and step progress, preview frames, and each output the moment it is committed. It reconnects on its own if the connection drops.
Preview.to_pil() requires the optional Pillow extra: pip install "comfy-sdk[pil]". result() returns the finished job, or raises JobFailed with the node-level detail if execution failed. See the SDK README for your language for the full event catalog.

events() is not subscribe()

Three similarly named things exist across the two surfaces, and only one of them is on this page: There is no subscribe() on the Comfy Cloud client, and no job.subscribe() anywhere. The Cloud equivalent of subscribe is run(): submit and wait for a terminal state. The stream is a live feed, not a replayable log. It exists so you can render progress, not so you can rely on it for results. Polling the job is what is authoritative, and run(), wait(), and result() fall back to polling automatically. See Design Notes for why.

Tracing an output back to its workflow

Outputs carry the id of the job that produced them, so you can start from a file and work backwards without keeping a side table.
The same id is on an asset fetched on its own, so a file you found later still leads back to its job. It is None (TypeScript: undefined) for an asset you uploaded, which has no producing job. From the job you can ask for the workflow behind it. This works for a job you did not submit in this process, rehydrated by id:
Always branch on format. Which shape comes back depends on how the job was submitted, not on anything you control per request: Jobs you submit through the SDK always return api, because v2 submission has no version-pinning fields yet. That will change; the discriminator is there so your code does not have to.

Label jobs and find them later

A label is a key and a value you attach to a job when you submit it, such as client: acme or project: spring. Use labels to record which of your clients or projects a job ran for. You can then list the jobs for one client without keeping your own table of job ids. Labels and the job list need the current SDK release, the one named in the version note under Install. Labels work on jobs sent to a Comfy API deployment, so set COMFY_BASE_URL to the deployment’s address as shown in Choosing a base URL. Comfy Cloud refuses them for now, and answers the job list with 501 and code not_implemented. A self-hosted comfy-api-proxy does not keep them either: its own metadata field is a single text string, so it refuses a label map. Its job list ignores label filters, so an SDK list call with labels returns no jobs there, and the same filters over HTTP return every job. The limits:
  • Up to 16 labels on a job.
  • A key is 1 to 40 characters: letters, digits, _, - and ..
  • A value is a string of up to 256 bytes (UTF-8).
  • Labels are set when you submit the job and cannot be changed later.
A submit that breaks a limit fails with 422 and code metadata_invalid, and no job is created. The error message names the key that broke the rule, or gives the count when there are more than 16 labels. The SDKs do not check the limits themselves. They raise an error that carries the server’s code and message.
Anyone in your workspace who can see a job can see its labels. Do not put secrets or personal data in them.
Pass metadata when you submit. The job carries the labels when you read it back:
A job submitted without labels has an empty metadata. To find the jobs for one client, list jobs filtered by label. The list covers only the jobs sent to the deployment at your base URL. Results come newest first, and the SDK fetches the next page for you as you iterate:
You can filter on up to 3 labels at once, and a job must match all of them exactly. List items do not include outputs. To get a job’s outputs, read the job by its id with client.jobs.get(job.id).

Over HTTP

Send metadata in the body of POST /api/v2/jobs:
Filter GET /api/v2/jobs with one metadata[<key>]=<value> query parameter per label. The -g flag stops curl from reading the square brackets as a pattern:
The response holds a page of jobs and, when more pages exist, a next_cursor. Pass it back as cursor, together with the same metadata[...] filters, to get the next page. A cursor sent without its filters returns unfiltered jobs. Use limit to set the page size (at most 500).
More than 3 label filters, or a key that breaks the key rule, fails with 400 and code invalid_metadata_filter. See the Comfy API v2 reference for every field on the job and the list.

What the SDKs cover today

The first version does one thing properly: run a workflow and get the results back.
  • Assets. Create input handles from a file, bytes, a stream, or a URL. Handles are lazy and content addressed, so re-running with the same input does not re-upload it.
  • Submission. Submit an API-format graph. Submission is idempotent, and a full queue is retried for you within a bounded budget.
  • Execution. Poll with wait(), or follow events() for live progress.
  • Outputs. Write to disk, buffer into memory, fetch a byte range, or get a short-lived download URL. getDownloadUrl() hands back a signed URL that anyone can read without an API key, valid for roughly 6 hours. Read Output URLs and How Long They Last before you store one: to keep showing an output later, re-host the bytes or re-mint the URL on demand.
  • Traceability. Every output carries the id of the job that produced it, and a job can hand back the workflow behind it.
  • Deleting assets. Remove an asset you uploaded, by handle or by id.
  • Errors. Typed exceptions such as JobFailed, Unauthorized, InsufficientCredits, and QueueFull, rather than raw status codes.
  • Cancellation. Jobs can be canceled while running. TypeScript additionally accepts an AbortSignal on any call.
Python ships both a synchronous Comfy client and an AsyncComfy client with the same surface. TypeScript is async only. Not in this version: managing saved workflows, the model library, node introspection, and named workflow parameters. Design Notes explains why the surface starts this small.

Reference

The SDK READMEs are the full reference for each language, including auth, assets, errors, and the low-level escape hatches.

Python SDK

comfy-sdk on PyPI. Sync and async clients.

TypeScript SDK

@comfyorg/sdk on npm. Typed, async, with a low-level client.

Comfy API v2 Reference

The HTTP API underneath both SDKs. Use it directly from any language.

Design Notes

Why this API exists, how it relates to the existing ComfyUI APIs, and what comes next.

Comfy Router

The other surface in the same package: comfy.models.run against partner models such as Flux, Veo and Gemini.

Feedback

Report what is awkward, what you expected to find and did not, and what you had to work around. The #developer-platform channel in our Discord is the place for it. If you want a first-party SDK in another language, say so there. Both SDKs sit on the same documented HTTP contract, so any language can talk to the API today, but we would rather know where the demand is.