Skip to content

API Reference

Itervox exposes a REST + Server-Sent Events (SSE) API on the dashboard port (default :8090). The web dashboard, TUI, and any custom integrations talk to the same endpoints.

http://localhost:8090/api/v1

When Itervox binds to a non-loopback address, it secures all /api/v1/* routes except /health with bearer-token auth.

  • If ITERVOX_API_TOKEN is set, that value is required.
  • If ITERVOX_API_TOKEN is unset and server.allow_unauthenticated_lan is false, Itervox auto-generates an ephemeral token at startup.
  • Loopback binds (127.0.0.1, localhost, ::1) do not require auth.
  • GET /health is always auth-exempt.
Terminal window
curl -H "Authorization: Bearer $ITERVOX_API_TOKEN" \
http://localhost:8090/api/v1/state

The dashboard captures ?token=<token> on first load, stores it in sessionStorage (or localStorage when Remember on this device is enabled), and sends Authorization: Bearer on both fetch and SSE requests.

The daemon-backed agent-action routes use a separate bearer token model. They are authenticated with a short-lived per-run action grant, not the main API token. These routes are:

  • POST /agent-actions/{identifier}/comment
  • POST /agent-actions/{identifier}/comment_pr
  • POST /agent-actions/{identifier}/create-issue
  • POST /agent-actions/{identifier}/move-state
  • POST /agent-actions/{identifier}/provide-input

Profiles opt into these capabilities with allowed_actions and create_issue_state in WORKFLOW.md.


JSON errors use the typed envelope below:

{
"error": {
"code": "bad_request",
"message": "message is required",
"field": "message"
}
}
  • field is optional and is mainly used by settings/forms.
  • Some legacy streaming code paths still use plain text responses for transport failures before SSE framing starts.

Auth-exempt liveness probe.

Response 200

{ "status": "ok" }

Full StateSnapshot SSE stream.

  • Event shape: data: <JSON StateSnapshot>\n\n
  • Initial snapshot is sent immediately.
  • Named keepalive event event: keepalive with data: {} after 25 seconds of stream inactivity.

Per-issue in-memory log SSE stream.

  • Event name: log
  • Event shape: id: <cursor>\nevent: log\ndata: <JSON IssueLogEntry>\n\n
  • Supports resume via Last-Event-ID
  • If the underlying in-memory buffer is cleared, stale cursors replay from the beginning of the current buffer

Per-issue session/subagent log SSE stream.

  • Event name: sublog
  • Event shape: id: <cursor>\nevent: sublog\ndata: <JSON IssueLogEntry>\n\n
  • Supports resume via Last-Event-ID
  • On mid-stream fetch failure the server emits:
event: error
data: {"code":"fetch_failed","message":"..."}

Global daemon-log SSE tail.

  • Event name: log
  • Initial connection sends the last ~16 KiB of the rotating daemon log file.
  • Optional query param identifier=<ISSUE-ID> filters matching log lines.

Returns the current orchestrator snapshot as JSON.

Response 200: StateSnapshot

Useful top-level fields include:

  • running, history, retrying, paused
  • inputRequired
  • availableProfiles, profileDefs
  • automations
  • automationQueue, automationQueueBackpressure
  • dependencyAudit, dependencyGraphNodes, dependencyGraphEdges
  • sshHosts, dispatchStrategy
  • autoClearWorkspace, inlineInput
  • configInvalid

MethodPathResponse
GET/issuesTrackerIssue[]
GET/issues/{identifier}TrackerIssue
MethodPathRequest bodySuccess responseNotes
DELETE/issues/{identifier}{"cancelled":true,"identifier":"ENG-1"}Alias for cancel
POST/issues/{identifier}/cancel{"cancelled":true,"identifier":"ENG-1"}404 not_running if not running
POST/issues/{identifier}/resume{"resumed":true,"identifier":"ENG-1"}404 not_paused if not paused
POST/issues/{identifier}/reanalyze{"queued":true,"identifier":"ENG-1"}404 not_paused if not paused
POST/issues/{identifier}/terminate{"terminated":true,"identifier":"ENG-1"}404 not_found if not running or paused
POST/issues/{identifier}/ai-review202 {"queued":true,"identifier":"ENG-1"}Reviewer dispatch
PATCH/issues/{identifier}/state{"state":"In Review"}{"ok":true,"identifier":"ENG-1","state":"In Review"}Triggers immediate refresh
MethodPathRequest bodySuccess response
POST/issues/{identifier}/profile{"profile":"frontend"} or {"profile":""}{"ok":true,"identifier":"ENG-1","profile":"frontend"}
POST/issues/{identifier}/backend{"backend":"claude"} or {"backend":""}{"ok":true,"identifier":"ENG-1","backend":"claude"}
POST/issues/{identifier}/provide-input{"message":"..."}{"ok":true}
POST/issues/{identifier}/dismiss-input{"ok":true}

provide-input / dismiss-input return 404 not_found when the issue is not currently in input_required.


MethodPathResponse
GET/issues/{identifier}/logsIssueLogEntry[]
GET/issues/{identifier}/sublogsIssueLogEntry[]
GET/logs/identifiersstring[]
MethodPathSuccess response
DELETE/issues/{identifier}/logs{"ok":true}
DELETE/issues/{identifier}/sublogs{"ok":true}
DELETE/issues/{identifier}/sublogs/{sessionId}{"ok":true}
DELETE/logs{"ok":true}

Notes:

  • /issues/{identifier}/logs reads the in-memory orchestrator log buffer.
  • /issues/{identifier}/sublogs reads persisted agent session logs and returns an empty array when no logs exist.
  • /logs is an SSE stream of the daemon log file, not a JSON endpoint.

All settings endpoints persist back to WORKFLOW.md.

MethodPathRequest bodySuccess response
POST/settings/workers{"workers":5} or {"delta":1}{"workers":5}
POST/settings/inline-input{"enabled":true}{"ok":true}
POST/settings/workspace/auto-clear{"enabled":true}{"ok":true,"autoClearWorkspace":true}
PUT/settings/tracker/states{"activeStates":[...],"terminalStates":[...],"completionState":"Done"}{"ok":true}
PUT/settings/tracker/failed-state{"failedState":"Failed"} (empty string = pause instead){"ok":true,"failedState":"Failed"}
PUT/settings/agent/max-retries{"maxRetries":5}{"ok":true,"maxRetries":5}
PUT/settings/agent/max-switches-per-issue-per-window{"maxSwitchesPerIssuePerWindow":2}{"ok":true,"maxSwitchesPerIssuePerWindow":2}
PUT/settings/agent/switch-window-hours{"switchWindowHours":6}{"ok":true,"switchWindowHours":6}
POST/settings/ssh-hosts{"host":"builder-1","description":"GPU box"}{"ok":true}
DELETE/settings/ssh-hosts/{host}{"ok":true}
PUT/settings/dispatch-strategy{"strategy":"round-robin" | "least-loaded"}{"ok":true}
DELETE/workspaces202 {"ok":true}
POST/refresh202 {"queued":true,"queued_at":"..."}
POST/automations/{id}/test{"identifier":"ENG-42"} (target issue identifier){"ok":true}

The skills endpoints expose the Settings -> Skills inventory and recommendation surface. They use the same dashboard bearer token as the rest of /api/v1.

MethodPathRequest bodySuccess responseNotes
GET/skills/inventoryInventory503 inventory_unavailable before the first successful scan
POST/skills/scanInventoryForces a fresh filesystem scan
GET/skills/issuesInventoryIssue[]Static analyzer recommendations
POST/skills/fix{"issueID":"UNUSED_PROFILE","fix": Fix}{"status":"ok"}v0.2.0 only applies the non-destructive UNUSED_PROFILE edit-yaml fix; unsafe actions such as remove-mcp are rejected
GET/skills/analyticsAnalyticsSnapshotIncludes HasRuntimeEvidence; 503 analytics_unavailable only before analytics can be computed
GET/skills/analytics/recommendationsRecommendation[]Runtime-side recommendations; empty until HasRuntimeEvidence is true

Inventory is a direct inventory/recommendation snapshot, not a complete normalized capability graph in v0.2.0. The response includes ScanTime, Partial/ScanError for best-effort scanner failures, and Stale when a tracked core config or discovered inventory file changed or disappeared since the scan. ORPHAN_MCP scans skill names, frontmatter descriptions, and skill bodies. Duplicate MCP recommendations are advisory because the daemon does not rewrite user-owned MCP settings files.


Profiles, reviewer, models, and automations

Section titled “Profiles, reviewer, models, and automations”
MethodPathRequest bodySuccess response
GET/settings/profiles{"profiles": { "<name>": ProfileDef }}
PUT/settings/profiles/{name}See body below{"ok":true}
DELETE/settings/profiles/{name}{"ok":true}

Profile update body:

{
"command": "codex --model gpt-5-codex",
"soul": "# qa SOUL\n\nYou are the QA specialist for this repository.",
"instructions": "# qa INSTRUCTIONS\n\nRun focused verification and report failures clearly.",
"soulFile": ".itervox/agents/qa/SOUL.md",
"instructionsFile": ".itervox/agents/qa/INSTRUCTIONS.md",
"backend": "codex",
"enabled": true,
"allowedActions": ["comment", "provide_input"],
"createIssueState": "Todo",
"originalName": "old-name"
}

For schema 2 profiles, soul and instructions are written to the referenced SOUL.md and INSTRUCTIONS.md files. prompt is retained as a compatibility field derived from instructions; new clients should use the file-backed fields.

MethodPathResponse
GET/settings/reviewer{"profile":"reviewer","auto_review":true}
PUT/settings/reviewer{"ok":true}
GET/settings/models{ "<backend>": [ModelOption] }

PUT /settings/reviewer request body:

{ "profile": "reviewer", "auto_review": true }
MethodPathRequest bodySuccess response
PUT/settings/automations{"automations":[AutomationDef]}{"ok":true}

There is no dedicated GET /settings/automations; the current list is exposed via GET /state / GET /events.

Supported trigger types:

  • cron
  • input_required
  • tracker_comment_added
  • issue_entered_state
  • issue_moved_to_backlog
  • run_failed
  • pr_opened — fires when a worker’s PR is detected
  • rate_limited — fires when a worker run exhausts retries and Itervox classifies the terminal failure as rate-limit-driven. The switch cap limits automated switching; it is not the trigger condition.
  • blockers_resolved — fires when dependency audit observes a previously blocked issue becoming unblocked.

Tracker event triggers are poll-derived, not webhook-derived. The automation loop runs every 15 seconds. tracker_comment_added compares only the latest observed comment, so multiple comments between polls collapse to the latest comment for trigger purposes.

Automation definitions preserve these optional policy/filter fields:

{
"id": "answer-stale-input",
"enabled": true,
"profile": "input-responder",
"trigger": { "type": "input_required" },
"filter": {
"maxAgeMinutes": 30,
"inputContextRegex": "tests|review"
},
"policy": {
"autoResume": true
}
}
{
"id": "rate-limit-switch",
"enabled": true,
"profile": "default",
"trigger": { "type": "rate_limited" },
"policy": {
"autoResume": true,
"switchToProfile": "fallback",
"switchToBackend": "codex",
"cooldownMinutes": 45
}
}

maxAgeMinutes is only valid for input_required triggers. rate_limited triggers require policy.autoResume: true for the automatic profile/backend switch. switchToProfile, switchToBackend, and cooldownMinutes are only valid for rate_limited triggers. blockers_resolved may set policy.moveToState; the selected profile must allow move_state before the daemon accepts that policy.

Validation failures return 400 with typed error codes such as:

  • duplicate_automation_id
  • invalid_cron
  • invalid_timezone
  • invalid_regex
  • invalid_trigger_type
  • invalid_match_mode
  • invalid_limit

These endpoints return 501 not_supported for trackers without project support.

MethodPathRequest bodySuccess response
GET/projects{"projects":[Project]}
GET/projects/filter{"filter":["alpha","beta"]} or {"filter":null}
PUT/projects/filter{"slugs":["alpha","beta"]} or {}{"filter":[...],"ok":true}

Notes:

  • Empty array means “all issues”.
  • Omitting slugs resets to the WORKFLOW.md default.

These routes are intended for agent subprocesses that have been granted daemon-backed permissions through profile allowed_actions.

MethodPathRequest bodySuccess response
POST/agent-actions/{identifier}/comment{"body":"..."}{"ok":true}
POST/agent-actions/{identifier}/comment_pr{"summary":"...","findings":[{"path":"...","line":42,"severity":"warning","body":"..."}]}{"ok":true,"findings":N}
POST/agent-actions/{identifier}/create-issue{"title":"...","body":"..."}{"ok":true,"issue":{...}}
POST/agent-actions/{identifier}/move-state{"state":"Todo"}{"ok":true}
POST/agent-actions/{identifier}/provide-input{"message":"..."}{"ok":true}

These routes require an Authorization: Bearer <grant-token> header carrying a short-lived action grant for the specific issue and action.

When the daemon spawns a local agent subprocess for a profile that has any allowed_actions configured, the following environment variables are injected so the agent can call back into the daemon without operator-supplied secrets:

VariableDescription
ITERVOX_ACTION_TOKENThe short-lived per-run action grant. Pass as Authorization: Bearer $ITERVOX_ACTION_TOKEN on every /agent-actions/... call.
ITERVOX_DAEMON_URLThe base URL for the daemon (http://127.0.0.1:<port> by default). Build the action URL as $ITERVOX_DAEMON_URL/api/v1/agent-actions/$ITERVOX_ISSUE_IDENTIFIER/<action>.
ITERVOX_ISSUE_IDENTIFIERThe issue this run belongs to (e.g. ENG-42).
ITERVOX_CREATE_ISSUE_STATESet when allowed_actions includes create_issue; the tracker state for follow-up issues. Usually used as the state field in the create-issue body.
ITERVOX_RUN_IDThe orchestrator’s run ID for this dispatch — useful for correlating logs.

Profiles WITHOUT allowed_actions do not receive these env vars and the shim PATH is unchanged. SSH remote workers also do not receive these env vars or the local action shims in v0.2.0; the worker prompt warns that daemon-backed actions are unavailable remotely.

Denials on these routes use codes such as:

  • unauthorized
  • agent_action_denied
  • not_supported

{
"command": "claude",
"prompt": "# reviewer INSTRUCTIONS\n\nReview the change.",
"soul": "# reviewer SOUL\n\nYou are the code reviewer.",
"instructions": "# reviewer INSTRUCTIONS\n\nReview the change.",
"soulFile": ".itervox/agents/reviewer/SOUL.md",
"instructionsFile": ".itervox/agents/reviewer/INSTRUCTIONS.md",
"backend": "claude",
"enabled": true,
"allowedActions": ["comment", "move_state"],
"createIssueState": "Todo"
}

soulFile and instructionsFile mirror the WORKFLOW.md profile references. The dashboard profile editor uses soul and instructions as the authoritative editable text for schema 2 profiles.

{
"id": "qa-ready",
"enabled": true,
"profile": "qa",
"instructions": "Run the QA routine.",
"trigger": {
"type": "issue_entered_state",
"state": "Ready for QA"
},
"filter": {
"matchMode": "all",
"states": ["Ready for QA"],
"labelsAny": ["qa"],
"identifierRegex": "^ENG-",
"limit": 10,
"inputContextRegex": "continue|branch"
},
"policy": {
"autoResume": true
}
}

Returned by both GET /issues and GET /issues/{identifier}.

Important fields include:

  • orchestratorState: idle, running, retrying, paused, input_required, pending_input_resume
  • agentProfile, agentBackend
  • comments, labels, branchName, blockedBy
  • blockedByDetails: richer blocker metadata for UI/API clients. Each entry has identifier and may include state and url. blockedBy remains the compatibility string-array form.
{
"level": "INFO",
"event": "action",
"message": "Write — updated README.md",
"tool": "Write",
"detail": "{\"status\":\"completed\",\"exit_code\":0}",
"time": "12:34:56",
"sessionId": "abc123"
}
VariablePurpose
ITERVOX_API_TOKENExplicit bearer token for non-loopback API access.
LINEAR_API_KEYLinear API token referenced from WORKFLOW.md.
GITHUB_TOKENGitHub API token referenced from WORKFLOW.md.
ITERVOX_DRY_RUNSet to 1 to run the orchestrator without dispatching worker subprocesses.