Skip to main content
Beta. The SDKs and the Comfy API v2 they call are still pre-1.0. The shape of the API can change before it stabilizes. 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. 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.4.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.4 is a floor and accepts later minors, while @comfyorg/sdk@^0.4 is npm’s compatible range for 0.4.x and stops short of 0.5.0, so a caret range needs raising when a new minor ships. Use @comfyorg/sdk@>=0.4.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 Parallel Execution.

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. 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

During the beta, the v2 API is served 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, authentication, and why the proxy exists. It is a stopgap: once the v2 API stabilizes it moves into ComfyUI core and the proxy is no longer needed.

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.

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

The SDKs are pre-1.0. Method names, the client shape, the event catalog, the error taxonomy and asset handling can all still change, and the surface is expected to stabilize over the next few weeks. Once it does, the long-term support commitment constrains what can be changed. 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.