diff --git a/datumctl/api-proxy.mdx b/datumctl/api-proxy.mdx
new file mode 100644
index 0000000..c260dbe
--- /dev/null
+++ b/datumctl/api-proxy.mdx
@@ -0,0 +1,100 @@
+---
+title: "Local API proxy"
+sidebarTitle: "Local API proxy"
+description: "Run a local authenticated proxy so any tool on your machine can talk to the Datum Cloud API without handling tokens."
+---
+
+`datumctl api proxy` starts a local HTTP proxy that forwards every request to the Datum Cloud API endpoint of your session, adding your credentials automatically and refreshing them as needed. Point any local tool — a dev server, a test harness, `curl` — at the printed URL and it can talk to the platform with no tokens to copy and no expiry to manage.
+
+```bash
+# Start a proxy on a fixed port, then call the API through it
+datumctl api proxy --port 8001
+curl http://127.0.0.1:8001/apis/resourcemanager.miloapis.com/v1alpha1/organizations
+```
+
+
+ Running in CI or an unattended pipeline? Use a [service account](/datumctl/auth/service-accounts) for the session the proxy serves — the proxy carries whatever identity you are logged in as.
+
+
+## Start the proxy
+
+```bash
+# Start a proxy on a fixed port for a dev server
+datumctl api proxy --port 8001
+
+# Start on a random free port; the URL is printed on the first stdout line
+datumctl api proxy
+
+# Serve a session other than your active one (see 'datumctl auth list' for names)
+datumctl api proxy --session sam@datum.net@api.staging.env.datum.net
+
+# Suppress per-request log lines
+datumctl api proxy --quiet
+```
+
+By default the proxy picks a random free port, so starting a second proxy never fails. Two things are always printed once the listener is ready:
+
+- A human-readable banner on **stderr** names the session, upstream, scope, and listen address, so you can verify at a glance whose credentials the proxy serves.
+- The bare proxy URL (for example `http://127.0.0.1:52347`) is printed as the **first and only line on stdout**. Scripts and test harnesses can read that one line as their readiness signal.
+
+```go
+cmd := exec.Command("datumctl", "api", "proxy", "--quiet", "--project", testProject)
+stdout, _ := cmd.StdoutPipe()
+_ = cmd.Start()
+apiURL, _ := bufio.NewReader(stdout).ReadString('\n') // first line = ready + address
+```
+
+Press `Ctrl+C` to stop the proxy. In-flight requests get a short grace period before their connections are closed.
+
+## Paths and scoping
+
+By default the proxy is a pure passthrough of the platform API surface: the same paths that work against the real API endpoint work against the local port, including the scoped organization and project control-plane prefixes. Swapping one base URL gives a tool dev/prod parity with nothing else to change.
+
+```bash
+# Watch DNS zones on a project control plane through the proxy
+curl "http://127.0.0.1:8001/apis/resourcemanager.miloapis.com/v1alpha1/projects/my-project/control-plane/apis/networking.datumapis.com/v1alpha/dnszones?watch=true"
+```
+
+With an explicit `--project` or `--organization` flag, the proxy instead serves that single control plane at its root, so URLs lose the long control-plane prefix:
+
+```bash
+datumctl api proxy --port 8001 --project my-project
+curl "http://127.0.0.1:8001/apis/networking.datumapis.com/v1alpha/dnszones?watch=true"
+```
+
+
+ The session and scope are **pinned when the proxy starts** and shown in the banner. Switching your active account with [`datumctl auth switch`](/datumctl/auth/managing-accounts) or your context with `datumctl ctx use` does not affect a running proxy, and the proxy never inherits a scope from your current context — scoping is always an explicit flag. Restart the proxy to pick up a new session or scope.
+
+
+## Streaming
+
+Streaming responses — watch requests, server-sent events, chunked transfer — pass through unbuffered: each event reaches your client the moment the upstream sends it, and the proxy never limits how long a response stays open. This makes the proxy suitable for long-lived watch clients.
+
+## Credentials and token refresh
+
+Every proxied request carries a fresh access token for the pinned session; the proxy refreshes the token before it expires and persists refreshed tokens exactly as other `datumctl` commands do. Service-account sessions work the same way.
+
+If the session cannot be refreshed — expired, revoked, or logged out with `datumctl logout` — the proxy stays up and answers requests with a synthesized `502 Bad Gateway` carrying a JSON status body, an `X-Datum-Proxy-Error: true` marker header, and a message telling you to run `datumctl login`. The proxy recovers automatically, without a restart, once you log back in to the same session.
+
+
+ A `401` or `403` from the platform itself passes through unchanged and without the marker header, so your client can always tell a proxy-local authentication problem from a real platform answer.
+
+
+## Security model
+
+- **Loopback only.** The proxy listens on `127.0.0.1` and there is no flag to bind other addresses. Anything that should be reachable remotely deserves a tunnel whose security model you own.
+- **Local clients are trusted.** Like other local developer proxies, the proxy does not authenticate local clients: while it runs, any process on your machine can make API calls as the pinned session. The banner names the identity it serves, requests are logged by default, and the credential itself is never exposed — a local client can act through the proxy but cannot take your token with it.
+- **Host-header validation.** Requests whose `Host` is not `localhost`, `127.0.0.1`, or `[::1]` are rejected with `403`, which defeats DNS-rebinding attacks from web pages.
+- **No CORS headers.** Browsers refuse scripted cross-origin reads of proxy responses; the proxy is meant for server-side and command-line clients.
+- **Authorization discipline.** Any `Authorization` header your local client sends is stripped and replaced with the session's real token, and tokens never appear in the request log.
+
+## Request logging
+
+One line per request is written to stderr — silence it with `--quiet`:
+
+```
+10:42:03 GET /apis/resourcemanager.miloapis.com/v1alpha1/organizations 200 143ms 8.1kB
+10:42:05 GET /apis/…/projects/my-project/control-plane/…/portalplugins?watch=true 200 …streaming
+```
+
+Streaming responses log once when headers arrive (marked `…streaming`) and again when the stream ends, with total duration and bytes — so an abruptly closed watch is visible.
diff --git a/docs.json b/docs.json
index deb6af9..20c4b2b 100644
--- a/docs.json
+++ b/docs.json
@@ -173,6 +173,15 @@
"datumctl/resources/safe-changes"
]
},
+ {
+ "group": "Plugins",
+ "pages": [
+ "datumctl/plugins/using-plugins",
+ "datumctl/plugins/adding-catalogs",
+ "datumctl/plugins/publishing-catalogs",
+ "datumctl/plugins/building-plugins"
+ ]
+ },
{
"group": "Activity & audit",
"pages": [
@@ -195,19 +204,16 @@
]
},
{
- "group": "CI/CD",
+ "group": "Developer tooling",
"pages": [
- "datumctl/cicd/github-actions",
- "datumctl/cicd/automating"
+ "datumctl/api-proxy"
]
},
{
- "group": "Plugins",
+ "group": "CI/CD",
"pages": [
- "datumctl/plugins/using-plugins",
- "datumctl/plugins/adding-catalogs",
- "datumctl/plugins/publishing-catalogs",
- "datumctl/plugins/building-plugins"
+ "datumctl/cicd/github-actions",
+ "datumctl/cicd/automating"
]
},
{