diff --git a/README.md b/README.md
index c4dfc8be1..6541cefd4 100644
--- a/README.md
+++ b/README.md
@@ -382,6 +382,12 @@ Use `--tls-keyfile key.pem --tls-certfile cert.pem` to enable TLS/SSL, the app w
> Note: Windows users can use [alexisrolland/docker-openssl](https://github.com/alexisrolland/docker-openssl) or one of the [3rd party binary distributions](https://wiki.openssl.org/index.php/Binaries) to run the command example above.
If you use a container, note that the volume mount `-v` can be a relative path so `... -v ".\:/openssl-certs" ...` would create the key & cert files in the current directory of your command prompt or powershell terminal.
+## How to browse the HTTP API?
+
+Launch with `--enable-api-docs` to serve the OpenAPI specification at `/openapi.yaml` and a browsable API reference at `/api-docs`.
+
+Both are off by default. The reference page loads its viewer from a CDN, so it needs outbound network access to render; the specification itself is served locally and can be read with any offline viewer, such as `npx @redocly/cli preview-docs openapi.yaml`.
+
## Support and dev channel
[Discord](https://comfy.org/discord): Try the #help or #feedback channels.
diff --git a/app/api_docs.py b/app/api_docs.py
new file mode 100644
index 000000000..48ecd50de
--- /dev/null
+++ b/app/api_docs.py
@@ -0,0 +1,59 @@
+"""Serves openapi.yaml and renders it as browsable API docs.
+
+Uses Redoc, not Swagger UI: the local server is unauthenticated by default, so
+the viewer must not be able to execute requests.
+"""
+
+import os
+
+from aiohttp import web
+
+SPEC_PATH = os.path.join(
+ os.path.dirname(os.path.dirname(os.path.realpath(__file__))), "openapi.yaml"
+)
+
+# The spec URL is relative so it resolves from both /api-docs and /api/api-docs.
+# The viewer is pinned and loaded from a CDN, so the page needs network access;
+# if it fails to load, the fallback below points at the locally served spec.
+API_DOCS_HTML = """
+
+
+
+
+ ComfyUI API Reference
+
+
+
+
+ The API docs viewer is loaded from a CDN and could not be reached. The
+ specification itself is served locally:
openapi.yaml
+
+
+
+
+"""
+
+
+def add_api_docs_routes(routes: web.RouteTableDef) -> None:
+ """Register /openapi.yaml and /api-docs on the given route table."""
+
+ @routes.get("/openapi.yaml")
+ async def get_openapi_spec(request):
+ # The spec is edited in place during development, so never let the
+ # browser hold a stale copy. no-cache still allows a 304 via the ETag
+ # FileResponse sets, which matters for a ~230 KB file.
+ return web.FileResponse(SPEC_PATH, headers={
+ "Content-Type": "application/yaml",
+ "Cache-Control": "no-cache",
+ })
+
+ @routes.get("/api-docs")
+ async def get_api_docs(request):
+ return web.Response(
+ text=API_DOCS_HTML,
+ content_type="text/html",
+ headers={"Cache-Control": "no-cache"},
+ )
diff --git a/comfy/cli_args.py b/comfy/cli_args.py
index c6660846d..97d977639 100644
--- a/comfy/cli_args.py
+++ b/comfy/cli_args.py
@@ -66,6 +66,7 @@ parser.add_argument("--tls-keyfile", type=str, help="Path to TLS (SSL) key file.
parser.add_argument("--tls-certfile", type=str, help="Path to TLS (SSL) certificate file. Enables TLS, makes app accessible at https://... requires --tls-keyfile to function")
parser.add_argument("--enable-cors-header", type=str, default=None, metavar="ORIGIN", nargs="?", const="*", help="Enable CORS (Cross-Origin Resource Sharing) with optional origin or allow all with default '*'.")
parser.add_argument("--max-upload-size", type=float, default=100, help="Set the maximum upload size in MB.")
+parser.add_argument("--enable-api-docs", action="store_true", help="Serve the OpenAPI spec at /openapi.yaml and browsable API docs at /api-docs. The docs page loads its viewer from a CDN, so it needs outbound network access to render.")
parser.add_argument("--base-directory", type=str, default=None, help="Set the ComfyUI base directory for models, custom_nodes, input, output, temp, and user directories.")
parser.add_argument("--extra-model-paths-config", type=str, default=None, metavar="PATH", nargs='+', action='append', help="Load one or more extra_model_paths.yaml files.")
diff --git a/server.py b/server.py
index c9ffcaa0d..b8f7378d5 100644
--- a/server.py
+++ b/server.py
@@ -42,6 +42,7 @@ from comfy_api import feature_flags
from comfy.comfy_api_env import get_environment_overrides
import node_helpers
from comfyui_version import __version__
+from app.api_docs import add_api_docs_routes
from app.frontend_management import FrontendManager, parse_version
from comfy_api.internal import _ComfyNodeInternal
from app.assets.seeder import asset_seeder
@@ -1223,6 +1224,8 @@ class PromptServer():
self.custom_node_manager.add_routes(self.routes, self.app, nodes.LOADED_MODULE_DIRS.items())
self.subgraph_manager.add_routes(self.routes, nodes.LOADED_MODULE_DIRS.items())
self.node_replace_manager.add_routes(self.routes)
+ if args.enable_api_docs:
+ add_api_docs_routes(self.routes)
self.app.add_subapp('/internal', self.internal_routes.get_app())
# Prefix every route with /api for easier matching for delegation.
diff --git a/tests-unit/server_test/test_api_docs.py b/tests-unit/server_test/test_api_docs.py
new file mode 100644
index 000000000..2b8940860
--- /dev/null
+++ b/tests-unit/server_test/test_api_docs.py
@@ -0,0 +1,70 @@
+"""Tests for the OpenAPI spec and API docs routes."""
+
+import pytest
+import pytest_asyncio
+import yaml
+from aiohttp import web
+
+from app.api_docs import add_api_docs_routes
+
+pytestmark = pytest.mark.asyncio
+
+
+@pytest_asyncio.fixture
+async def client(aiohttp_client):
+ app = web.Application()
+ routes = web.RouteTableDef()
+ add_api_docs_routes(routes)
+ app.add_routes(routes)
+ return await aiohttp_client(app)
+
+
+async def test_get_spec(client):
+ resp = await client.get("/openapi.yaml")
+ assert resp.status == 200
+ assert resp.headers["Content-Type"] == "application/yaml"
+ assert resp.headers["Cache-Control"] == "no-cache"
+ spec = yaml.safe_load(await resp.text())
+ assert spec["openapi"].startswith("3.")
+ assert spec["paths"]
+
+
+async def test_missing_spec_returns_404(client, monkeypatch):
+ monkeypatch.setattr("app.api_docs.SPEC_PATH", "/nonexistent/openapi.yaml")
+ resp = await client.get("/openapi.yaml")
+ assert resp.status == 404
+
+
+async def test_docs_page(client):
+ resp = await client.get("/api-docs")
+ assert resp.status == 200
+ assert resp.content_type == "text/html"
+ body = await resp.text()
+ # Redoc has no request execution; Swagger UI's "Try it out" would give
+ # one-click access to destructive endpoints on an unauthenticated server.
+ assert "