HTTP API
Every route harnessd serves, with its method, path, minimum role, request body and response; plus the Mac app's local loopback API.
harnessd serves a JSON API over HTTPS on its listen address. The Mac app uses it for Remote mode, and you can call it from scripts. Every request is authenticated, role-checked, and every write is audited. The Mac app also serves a small, unauthenticated API on loopback; see Local mode API.
Basics#
Base URL: https://<listen address>, for example https://harness.internal:8443. Clients should pin the server's certificate (see TLS).
Authentication: sign in with POST /auth/login, then send the token on every request:
curl --cacert fullchain.pem https://harness.internal:8443/api/v1/whoami \
-H "Authorization: Bearer hs_…"Roles: each route below shows the minimum role. viewer < operator < admin. A request without a valid session gets 401 unauthenticated; one with too low a role gets 403 forbidden ("requires the operator role").
Step-up codes: routes that change secrets, test logins, server settings or app-added projects also need totp, a fresh code from the admin's authenticator, in the body. Each code works once.
Errors are JSON with a code and a message:
{ "error": { "code": "forbidden", "message": "requires the admin role" } }Some add details, such as problems (a list of validation messages) or version.
Limits and rules:
- Requests with an
Originheader are refused:403 origin_not_allowed. There's no browser client. - Request bodies are capped at 1 MiB (
413 payload_too_large). - Long-poll routes wait up to 25 seconds for a change.
- Path segments (project names, task identifiers, variable names) are percent-decoded.
Authentication#
/auth/loginNo authSigns in. No token needed.
{ "username": "maria", "password": "…", "totp": "123456" }totp is required for accounts with an authenticator, and for admins when require_totp_for_admins is on.
{ "token": "hs_…", "role": "operator", "expires_in": 43200, "idle_timeout": 3600 }| Status | code | When |
|---|---|---|
| 400 | bad_request | Missing username or password |
| 401 | totp_required | Right password; send the code |
| 401 | invalid_credentials | Anything else, without saying which part was wrong |
| 429 | locked | Too many failed attempts for the account or IP |
/auth/logoutviewerRevokes the current session. Returns { "ok": true }.
/healthzNo authLiveness check without details. Returns { "ok": true }.
/api/v1/whoamiviewerReturns { "username": "maria", "role": "operator" }.
Projects#
/api/v1/projectsviewerLists projects with a summary of each.
{
"projects": [
{
"name": "shop-storefront",
"status": "running",
"source": "file",
"running": 2,
"retrying": 0,
"tracker": "ClickUp · Sprint 42",
"config_errors": [],
"preflight_error": null,
"limit_block": null,
"auto_paused": null,
"spend_today": { "cost_usd": 12.4, "tokens": 1830000, "runs": 9 },
"parked": [
{ "issue_id": "86b1x", "issue_identifier": "CU-86b1x", "reason": "stopped after 5 failed attempts in a row …",
"attempts": 5, "cost_usd": 3.1, "at": "2026-11-06T08:12:00Z" }
]
}
]
}source is file for projects in harnessd.yaml and harness for projects added from the app.
/api/v1/projectsadminAdds a project from a repository URL. The server clones it into data_dir/repos/<name>, checks the workflow file, stores the project in its overlay and optionally starts it. Needs totp.
{
"name": "shop-storefront",
"repo": "git@github.com:your-org/shop-storefront.git",
"workflow": "WORKFLOW.md",
"auto_start": true,
"set": { "SENTRY_DSN": "https://…" },
"logins": [
{ "name": "shopper", "origins": ["http://localhost:3000"], "username": "qa@example.com", "password": "…" }
],
"totp": "123456"
}repo must be https://, ssh:// or user@host:path. workflow defaults to WORKFLOW.md and must be inside the repository. set and logins are optional: the project's variables and test logins.
Returns 201 with the project summary.
| Status | code | When |
|---|---|---|
| 400 | bad_project | Invalid name, URL, workflow path, variable or login; see problems |
| 409 | name_taken | A project with that name exists |
| 409 | busy | The same project is being added |
| 422 | add_failed | The clone failed, or the repository has no readable workflow file |
/api/v1/projects/<p>adminRemoves a project added from the app: stops it and deletes its variables and test logins. The clone stays. Body: { "totp": "123456" }. Returns { "removed": "<p>" }. Projects in harnessd.yaml get 409 defined_in_config.
/api/v1/projects/<p>/startadminStarts the project's orchestrator. Returns the project summary. Also /stop, /pause (stop dispatching; agents continue) and /resume (which also clears an automatic pause).
Board and live state#
/api/v1/projects/<p>/live?since=<version>viewerLong-poll for the project's live state. Returns at once if the state's version differs from since, otherwise waits up to 25 seconds.
{
"version": "a1b2c3d4e5f60718",
"project": { "name": "shop-storefront", "status": "running", "…": "…" },
"state": { "running": [ … ], "retrying": [ … ], "codex_totals": { … } },
"board": {
"columns": ["to do", "in progress", "review", "complete"],
"editable": true,
"issues": [
{ "id": "86b1x", "identifier": "CU-86b1x", "title": "…", "state": "in progress", "priority": 2,
"labels": ["agent"], "running": true, "parked": null, "waits_on": [] }
]
}
}Each issue has the prompt-template fields plus running, parked (the reason, or null) and waits_on (linked tasks it waits on).
/api/v1/projects/<p>/mirror?since=<version>viewerLong-poll for the full project as the Mac app mirrors it: configuration, running agents, retries, history and the workflow's text and version. Errors and messages are redacted. This is the app's internal format and may change.
/api/v1/projects/<p>/issues/<id>viewerOne task, by id or identifier, with comments (author, body, at) and linked_tasks (project, identifier, title, state, done, wait). 404 issue_not_found if it isn't on the board.
/api/v1/projects/<p>/issuesoperatorCreates a task.
{ "title": "Fix the cart total rounding", "description": "Markdown…", "priority": 2,
"state": "Todo", "labels": ["bug"], "blocked_by": ["HAR-3"] }Only title is required (up to 300 characters). state defaults to the first board column and must be one of them. priority is 0–4; up to 20 labels. Returns 201 { "identifier": "HAR-14" }. 409 read_only_board for GitHub projects; 502 tracker_error if the tracker refuses.
/api/v1/projects/<p>/issues/<id>/stateoperatorMoves a task. Body: { "state": "Todo" }, one of the board's columns (400 bad_state otherwise). Returns the issue.
/api/v1/projects/<p>/issues/<id>/commentsoperatorAdds a comment. Body: { "body": "Markdown" } (up to 50,000 characters). The comment is signed — <user> via SI Fabrik. Returns the issue.
/api/v1/projects/<p>/issues/<id>/dispatchoperatorDispatches the task now. 409 not_running if the project isn't started. Returns the issue.
/api/v1/projects/<p>/issues/<id>/release-waitsoperatorStops the task waiting on its linked tasks. Returns the issue.
/api/v1/projects/<p>/issues/<id>/resumeoperatorUn-parks a task a guardrail parked. Returns the project summary; 404 not_parked if it isn't parked.
Runs and transcripts#
/api/v1/projects/<p>/runs?limit=50viewerPersisted run history, newest first. limit is 1–200, default 50.
{
"project": "shop-storefront",
"runs": [
{ "id": "7f0c…", "issue_id": "86b1x", "issue_identifier": "CU-86b1x", "title": "…",
"outcome": "succeeded", "error": null, "started_at": "…", "ended_at": "…", "duration_s": 812.4,
"turns": 6, "runner": "claude_code", "tokens": { "input": 120000, "output": 18000, "total": 138000 },
"cost_usd": 2.31, "pull_request_url": "https://github.com/your-org/shop-storefront/pull/88", "delivery": "…" }
]
}/api/v1/projects/<p>/runs/<run_id>operatorOne run with its transcript: items with at, kind, title and body, redacted and trimmed as stored. 404 run_not_found.
/api/v1/projects/<p>/running/<id>/transcript?after=<n>operatorLong-poll for new transcript items of a running agent, after the first n.
{ "running": true, "identifier": "CU-86b1x", "total": 42, "phase": "streamingTurn",
"items": [ { "at": "…", "kind": "toolCall", "title": "Bash", "body": "npm test" } ] }When the agent isn't running, returns { "running": false, "total": <n>, "items": [] }. Bodies are redacted and capped at 64,000 characters.
/api/v1/runs/<issue_id>/stopoperatorStops the running agent for a task, in whichever project. Returns { "ok": true }, or 404 run_not_found.
Workflow#
/api/v1/projects/<p>/workflowoperatorReturns { "text": "---\ntracker: …", "version": "<sha256 of text>" }.
/api/v1/projects/<p>/workflowadminSaves WORKFLOW.md and hot-reloads the project.
{ "text": "---\n…", "base_version": "<version from GET>" }Returns { "version": "…" }.
| Status | code | When |
|---|---|---|
| 409 | changed | The file changed since base_version; the response includes the current version |
| 413 | too_large | Over 256 KB |
| 422 | invalid_workflow | It doesn't parse or validate, or it's below the sandbox floor; see problems |
Workspaces#
/api/v1/projects/<p>/workspacesoperatorThe project's task workspaces: [{ "key": "CU-86b1x", "branch": "symphony/CU-86b1x", "saved": true, "in_use": false, "modified": "…" }].
/api/v1/projects/<p>/workspaces/<key>adminSaves the workspace's task branch into the project repository, then removes it. Returns { "removed": "<key>" }. 404 not_found if there's no such workspace; 409 running if an agent is in it; 409 not_removed if the branch couldn't be saved.
Server settings#
/api/v1/serverviewerServer facts for the General tab: listen, tls, certificate_sha256, tools_listen, require_totp_for_admins, session, users, projects, docker, which credentials are set (booleans), and the effective gateway, limits and sandbox.
/api/v1/server/settingsviewerThe effective settings, where each section comes from, and (for admins) variable names.
{
"gateway": { "enabled_by_default": true, "base_url": "http://10.8.0.20:4000", "auth_header": "bearer",
"opus_model": "", "sonnet_model": "", "haiku_model": "claude-haiku-5-5", "codex_model": "",
"tag_requests": true, "disable_experimental_betas": false, "wire_api": "responses" },
"limits": { "max_concurrent_agents": 6, "daily_budget_usd": 200 },
"sandbox": { "allow": ["registry.npmjs.org", "pypi.org"], "cloudflare_bridge_url": null },
"sources": { "gateway": "file", "limits": "harness", "sandbox": "file" },
"variables": [ { "name": "CLICKUP_API_TOKEN", "source": "file" } ],
"can_edit": true
}/api/v1/server/settingsadminOne Save: any of the sections, server-wide variables and resets, under one code.
{
"limits": { "max_concurrent_agents": 8, "daily_budget_usd": 250 },
"reset": ["sandbox"],
"set": { "HARNESS_GATEWAY_API_KEY": "sk-…" },
"remove": ["OLD_TOKEN"],
"totp": "123456"
}gateway, limits and sandbox take the same shapes as the GET response. reset returns sections to harnessd.yaml. Returns the new settings. 400 bad_settings with problems if a value is invalid. An empty change returns the current settings without asking for a code.
/api/v1/server/gateway/testadminLists the gateway's models from the server. Body: { "base_url": "http://10.8.0.20:4000", "auth_header": "bearer", "key": "optional" } (without key, the saved key is used). Returns { "models": [...], "latency_ms": 42 }, or 502 gateway_unreachable.
/api/v1/server/sandbox/testadminStarts a throwaway Cloudflare sandbox with no network, runs a command and destroys it, with the saved token. Body: { "bridge_url": "optional" }. Returns { "ok": true, "message": "Connected — a sandbox started and ran a command" }.
Sandboxes#
/api/v1/server/sandboxesviewerThe server's Sandboxes screen: docker (engine info), running_containers, agent_image and browser_image (reference, present, size, built), containers (only this server's), build (building, browser, log, succeeded), cloudflare (bridge_url, token_set, healthy, relay_connected, relay_routes, relayed_requests, hub, projects) and kept Cloudflare workspaces.
/api/v1/server/containers/<id>/logsoperatorA container's logs, redacted: { "name": "…", "logs": "…" }. Only this server's containers (404 not_found otherwise). An id prefix of 12 or more characters works.
/api/v1/server/containers/<id>/stopoperatorStops and removes one of this server's containers. Returns { "stopped": "<name>" }, or 502 docker_error.
/api/v1/server/images/buildadminBuilds harness-agent:latest, or with { "browser": true } the browser image. Returns 202 { "building": true }; follow progress in /api/v1/server/sandboxes. 409 docker_unavailable or 409 building.
Secrets#
All secret routes are admin-only and write-only: responses contain names and sources, never values. Changes need totp.
/api/v1/secretsadminServer-wide variable names: { "variables": [ { "name": "CLICKUP_API_TOKEN", "source": "file" } ] }. source is file (from secrets.env) or harness (set from the app).
/api/v1/secrets/<NAME>adminSets a server-wide value in the overlay. Body: { "value": "…", "totp": "123456" }. One line, up to 16 KB. Returns the list. DELETE with { "totp": "…" } removes the overlay value, falling back to the file's.
/api/v1/projects/<p>/secretsadminThe project's variable names, and its test logins without passwords: name, username, origins, has_totp, problems.
/api/v1/projects/<p>/secrets/<NAME>adminSets a project value. Same body as server-wide. DELETE removes it.
/api/v1/projects/<p>/logins/<name>adminSaves a test login.
{ "origins": ["http://localhost:3000"], "username": "qa@example.com", "password": "…",
"totp_secret": "JBSWY3DPEHPK3PXP", "totp": "123456" }400 bad_login with problems if it isn't valid. DELETE with { "totp": "…" } removes it.
Common step-up errors:
| Status | code | When |
|---|---|---|
| 401 | totp_required | No code in the body |
| 401 | bad_totp | Wrong or already-used code |
| 403 | totp_not_enrolled | The admin has no authenticator (harnessd user totp <name>) |
| 429 | locked | Too many wrong codes |
Symphony state API#
The read API from the Symphony spec (§13.7), shared with Local mode.
/api/v1/stateviewerAggregated runtime state across projects: counts (running, retrying), running and retrying entries, codex_totals (input_tokens, output_tokens, total_tokens, seconds_running), rate_limits and per-project snapshots. Add ?project=<name> for one project.
/api/v1/<identifier>viewerRuntime detail for one task (for example /api/v1/HAR-12): its running or retry state, workspace, recent events and log paths. 404 issue_not_found if it isn't in the current runtime state.
/api/v1/refreshoperatorPolls and reconciles now. Returns 202 { "queued": true, "coalesced": false, "requested_at": "…", "operations": ["poll", "reconcile"] }. Repeated requests are coalesced.
Agent tools endpoint#
/mcp/<session-token>session tokenAn MCP streamable-HTTP endpoint (JSON responses) for one agent session, served only on tools_listen. It supports initialize, ping, tools/list and tools/call; DELETE ends the session, and SSE streams aren't supported. The token is per run and is injected into Claude Code with --mcp-config and into Codex through its mcp_servers config. Unknown or expired tokens get a JSON-RPC error. See Agent MCP tools.
Local mode API#
The Mac app serves this on http://127.0.0.1:7878 (change it in Settings → General). It's loopback only and has no authentication.
| Route | Description |
|---|---|
GET / | A minimal HTML dashboard: totals, running agents, retry queue |
GET /api/v1/state | As above; ?project=<name> narrows |
GET /api/v1/<identifier> | As above |
POST /api/v1/refresh | As above, 202 |
POST /mcp/<session-token> | The agents' tracker tools |