HTTP API
For scripting the controller from something that is not a browser.
Everything the web UI does goes through server functions rather than these routes. The HTTP API exists for scripting: requesting a workspace from a CI job, checking the fleet from a terminal, watching an agent from something that is not a browser.
Authentication
Send x-api-key: <key>. Create a key with:
pnpm build # apikey:create runs dist/cli/apikey.js, so build it first
pnpm apikey:create <name>It is printed once and stored only as a hash. There is no way to read it back.
Every route below requires it, except GET /api/health. With CONTROLLER_AUTH_SECRET unset
the controller has no authentication at all and answers anyone who can reach it, which is only
survivable on a trusted LAN. GET /api/health reports whether auth is configured, so that is the
one call worth making before trusting the rest.
Routes
GET /api/health | Controller health, whether auth is configured, and whether provisioning is on. The only unauthenticated route. |
GET /api/workspaces | Every workspace with its timeline. |
POST /api/workspaces | Request one. |
GET /api/workspaces/:id | One workspace, with placement, failure detail and its whole timeline. |
DELETE /api/workspaces/:id | Queue destruction. Returns once the operation is queued, not once the container is gone. |
POST /api/workspaces/:id/retry | Queue a fresh provision. Only for a workspace in failed. |
GET /api/workspaces/:id/agent | Server-sent events: the agent's transcript, live. |
GET /api/infrastructure/probe | Check the controller can reach Proxmox. |
Requesting a workspace
curl -X POST https://pve-agents.example.internal/api/workspaces \
-H "x-api-key: $PVE_AGENTS_KEY" \
-H "content-type: application/json" \
-d '{
"idempotencyKey": "ci-run-8813",
"harnessId": "b0f1e8a2-...",
"repository": "github.com/you/your-repo",
"ref": "main",
"purpose": "Work out why the nightly job leaves orphans behind"
}'| Field | ||
|---|---|---|
idempotencyKey | required | 1-255 chars. Replaying the same key returns the workspace the first call made, rather than building a second container. Replaying it with a different body is a conflict, not a new workspace. |
harnessId | required | Which configured agent to run it on. See below. |
repository | required | 1-2000 chars. |
ref | optional | 1-255 chars, defaults to main. |
purpose | optional | 1-500 chars. What the agent is told to do. Without it the workspace comes up ready and idle, waiting for a prompt. |
Where harnessId comes from
It is the id of a row in the Agents tab. Required, with no controller-wide default: picking an agent on the caller's behalf would run one they did not choose.
There is no HTTP route that lists configured agents, so a scripted caller cannot discover its
id — it has to be given one. listLaunchableHarnesses exists, but as a server function the browser
calls, not as a route. Read the id from the Agents tab and put it in the caller's configuration.
This is a gap rather than a design. A request without harnessId is refused by validation, which is
what someone upgrading an existing script will see first.
The response is the persisted workspace. It has been requested, not built: the worker picks it up
on its next pass, and the container appears some tens of seconds later. Poll
GET /api/workspaces/:id or watch the timeline to see how far it got.
With PROVISIONING_ENABLED=false the request is accepted and queued and nothing is ever built.
That is the intended way to exercise this API without touching Proxmox.
Watching an agent
GET /api/workspaces/:id/agent is an EventSource stream, not a plain response.
A reader gets the runner's whole snapshot first and the live events after it, because a transcript replay is not the same for a late arrival as it is for someone who was there from the start. One SSH attachment is made per reader rather than one shared between them.
The stream sends a comment line every 20 seconds. A thinking agent can say nothing for minutes, which a proxy or a browser otherwise treats as a dead connection.
If you put a proxy in front of the controller, exclude this path from buffering and from compression. Both hold the stream and deliver it in bursts, or not at all. See the reverse proxy section for the two rules that matter.
A workspace with nothing to watch is refused rather than streamed as empty, so a caller can tell the difference between "no agent here" and "an agent that has not spoken yet".
Last updated on