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.
Base URL
Section titled “Base URL”http://localhost:8090/api/v1Authentication
Section titled “Authentication”Dashboard / API bearer auth
Section titled “Dashboard / API bearer auth”By default, on every bind — including loopback (127.0.0.1, localhost,
::1) — Itervox secures all /api/v1/* routes except /api/v1/health and
/api/v1/ready with bearer-token auth (and /metrics, when enabled, with the
same token). Bind address is not treated as a security boundary: a
loopback daemon behind a tunnel or reverse proxy is exactly as reachable as
one bound to 0.0.0.0.
- If
ITERVOX_API_TOKENis set, that value is required. - If
ITERVOX_API_TOKENis unset andserver.allow_unauthenticatedisfalse(the default), Itervox auto-generates an ephemeral token at startup and prints the tokened dashboard URL to stderr once — only when stderr is a terminal orITERVOX_PRINT_TOKEN=1(--no-print-tokenalways suppresses it). Otherwise the token is written to<logs-dir>/api-token(mode 0600) and the log carries only its path and asha256:fingerprint. server.allow_unauthenticated: truedisables auth entirely, on every bind. Renamed fromserver.allow_unauthenticated_lan, which still parses as a deprecated alias.- With
server.allow_unauthenticated: true, state-changing requests (POST,PUT,PATCH,DELETE) from another origin are refused with403cross_origin_forbidden:Sec-Fetch-Site: cross-site/same-site, or anOriginwhose host and port differ fromHost(scheme is not compared;Origin: nullnever matches). Requests with neither header (curl, scripts) andGET/SSE pass. Token mode has no origin check. - With
server.allow_unauthenticated: true, every route (API, SSE, dashboard) refuses a request whoseHostis a DNS name other thanlocalhost,server.hostor one inserver.allowed_hostswith403host_not_allowed(DNS-rebinding guard). IP addresses always pass;GET /api/v1/healthandGET /api/v1/readyare exempt. Token mode has no Host check. GET /api/v1/healthandGET /api/v1/readyare always auth-exempt — the only auth-exempt routes.GET /metricsis never auth-exempt.
curl -H "Authorization: Bearer $ITERVOX_API_TOKEN" \ http://localhost:8090/api/v1/stateThe 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.
Agent-action bearer auth
Section titled “Agent-action bearer auth”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}/commentPOST /agent-actions/{identifier}/comment_prPOST /agent-actions/{identifier}/create-issuePOST /agent-actions/{identifier}/move-statePOST /agent-actions/{identifier}/merge_prPOST /agent-actions/{identifier}/provide-input
Profiles opt into these capabilities with allowed_actions and
create_issue_state in WORKFLOW.md.
Error format
Section titled “Error format”JSON errors use the typed envelope below:
{ "error": { "code": "bad_request", "message": "message is required", "field": "message" }}fieldis optional and is mainly used by settings/forms.- The dashboard surfaces
messagein its error toasts. A503whosecodeisorchestrator_busy,settings_reloadingordeps_override_queue_fullmeans nothing was enqueued or written; the dashboard retries such a request once afterRetry-After. Other503s are not retried. - Some legacy streaming code paths still use plain text responses for transport failures before SSE framing starts.
Health
Section titled “Health”GET /api/v1/health
Section titled “GET /api/v1/health”Auth-exempt liveness probe.
Response 200
{ "status": "ok" }A static answer: it proves the HTTP server is up, not that the orchestrator
is working. Use it for liveness (restart-on-failure); use /ready for
readiness and alerting.
GET /api/v1/ready
Section titled “GET /api/v1/ready”Auth-exempt readiness probe driven by the orchestrator’s event loop. 200
when ready, 503 when not; the body is the same either way and holds only
booleans and one timestamp:
{ "ready": true, "loop_fresh": true, "last_poll_ok": true, "config_invalid": false, "degraded": false, "tracker_rate_limited_until": null, "draining": false}| Field | Meaning |
|---|---|
ready | loop_fresh, fewer than 3 consecutive failed tracker polls, and not draining. |
loop_fresh | The event loop is alive. Idle, it must have finished an iteration within 3× polling.interval_ms (at least 30 s). Inside a tick or event — for example a slow tracker call — it stays fresh for up to 10 minutes (the tick budget). Within one poll interval of startup (at least 30 s) it is fresh before the first tick. |
last_poll_ok | The most recent tick polled the tracker and the poll succeeded (false before the first poll and while polling is shed for budget). |
config_invalid | The last WORKFLOW.md reload failed validation; the daemon keeps running on its last valid config. |
degraded | A rate-limited poll, an open tracker rate-limit gate, a tick that skipped its poll to keep the tracker budget for writes, 1–2 consecutive failed polls, or config_invalid. Degraded is still ready. |
tracker_rate_limited_until | RFC 3339 instant the tracker rate-limit gate lifts, null while it is closed. |
draining | The daemon is draining — after SIGTERM/SIGINT, or before applying an operator edit to WORKFLOW.md — and admits no new work while in-flight agent turns finish. A draining daemon answers 503 so a load balancer stops routing to it; loop_fresh stays true. |
A rate-limited poll does not count as a failure: the tracker said when to
come back, so the probe reports degraded and stays ready. config_invalid
does not make the daemon unready either — taking it out of a load balancer
would hide the dashboard you fix the file from. Like /health, /ready is
exempt from the unauthenticated-mode Host guard: its body carries no error
text, identifiers or configuration.
Metrics
Section titled “Metrics”GET /metrics
Section titled “GET /metrics”Prometheus text exposition (text/plain; version=0.0.4). Off by default:
set server.metrics.enabled: true. Requires the same bearer token as
/api/v1/* (in allow_unauthenticated mode it is open like the rest of the
API and subject to the Host guard). Disabled, it answers 404.
curl -H "Authorization: Bearer $ITERVOX_API_TOKEN" http://localhost:8090/metrics| Family | Type | Labels | Meaning |
|---|---|---|---|
itervox_workers_running | gauge | Agent workers running now. | |
itervox_workers_max | gauge | agent.max_concurrent_agents as of the last tick. | |
itervox_retry_queue_length | gauge | Issues waiting to retry. | |
itervox_automation_queue_length | gauge | Automation queue entries. | |
itervox_input_required | gauge | Issues waiting for human input. | |
itervox_paused_issues | gauge | Paused issues. | |
itervox_worker_exits_total | counter | reason | Worker exits (succeeded, failed, stalled, input_required, canceled_by_reconciliation). |
itervox_tracker_requests_total | counter | adapter, outcome | Tracker HTTP calls after in-call retries; outcome is ok, rate_limited or error (transport failure). |
itervox_goroutine_panics_total | counter | Panics recovered on daemon goroutines (the site is in the log line). | |
itervox_events_dropped_total | counter | Orchestrator events that could not be delivered (full channel or send timeout). | |
itervox_client_errors_total | counter | kind, outcome | Dashboard error reports on POST /api/v1/client-errors; kind is render, error, unhandledrejection, schema or other, outcome is accepted, rate_limited or dropped. |
itervox_persist_write_errors_total | counter | Failed runtime-ledger writes. Resets on a WORKFLOW.md reload. | |
itervox_transport_failures_total | counter | Retry-exhausted runs classified as transport failures. Resets on reload. | |
itervox_dispatch_ticks_total | counter | bound | Poll ticks observed (any), and those that were slot-bound (slot) or dependency-bound (dependency). Resets on reload. |
itervox_dispatch_eligible_waiting | gauge | Eligible issues waiting for a slot on the last tick. | |
itervox_dispatch_blocked_by_dependency | gauge | Candidates held by a dependency gate on the last tick. | |
itervox_outbox_entries | gauge | state | Outbox entries: pending (all), degraded, rate_limited. |
itervox_tracker_rate_limited_until_seconds | gauge | adapter | Unix time the tracker rate-limit gate lifts; 0 while closed. |
itervox_tracker_poll_consecutive_failures | gauge | Consecutive failed (non-rate-limited) candidate polls. | |
itervox_tracker_last_error_timestamp_seconds | gauge | kind | Unix time of the last tracker failure (outage or rate_limited); absent when none. |
itervox_event_loop_last_idle_timestamp_seconds | gauge | Unix time the event loop last finished an iteration. |
No series carries an issue identifier. The process-wide counters survive a
WORKFLOW.md reload; the ones marked “resets on reload” belong to the
orchestrator generation and restart from zero, which rate() handles as a
counter reset. A scrape never waits on a settings save: it reads the
orchestrator snapshot, the outbox and the rate-limit gate only.
Real-time streams
Section titled “Real-time streams”GET /events
Section titled “GET /events”Full StateSnapshot SSE stream.
- Event shape:
data: <JSON StateSnapshot>\n\n - Initial snapshot is sent immediately.
- Named keepalive event
event: keepalivewithdata: {}after 25 seconds of stream inactivity.
GET /issues/{identifier}/log-stream
Section titled “GET /issues/{identifier}/log-stream”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
GET /issues/{identifier}/sublog-stream
Section titled “GET /issues/{identifier}/sublog-stream”Per-issue session/subagent log SSE stream.
- Event name:
sublog - Event shape:
id: <epoch>-<seq>\nevent: sublog\ndata: <JSON IssueLogEntry>\n\n(CORE-153).epochidentifies the issue’s current set of session-log entries and changes when the session files are replaced or cleared. - Supports resume via
Last-Event-ID; a cursor from another epoch, a bare pre-CORE-153 number or one past the end getsid: <epoch>-0\nevent: gapand a full replay. - On mid-stream fetch failure the server emits:
event: errordata: {"code":"fetch_failed","message":"..."}GET /logs
Section titled “GET /logs”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.
GET /state
Section titled “GET /state”Returns the current orchestrator snapshot as JSON.
Response 200: StateSnapshot
Useful top-level fields include:
running,history,retrying,pausedinputRequiredavailableProfiles,profileDefsautomationsautomationQueue,automationQueueBackpressuredependencyAudit,dependencyGraphNodes,dependencyGraphEdgessshHosts,dispatchStrategyautoClearWorkspace,inlineInputconfigInvalidlastTrackerError— the most recent tracker failure, absent when none:{ "at", "op": "poll" | "update_state", "kind": "outage" | "rate_limited", "message", "resetAt"?, "consecutiveFailures"? }. A successful poll clears a poll failure; a failed failed-state move stays for up to an hour.outboxEntries[].lastFailedAt— when that outbox entry last failed to deliver (rate-limit deferrals do not count).backendHealth(CORE-055) — one row per agent backend, and per SSH host that has a breaker:{ "backend", "host"?, "status": "healthy" | "warning" | "limited" | "probing", "kind"?: "quota" | "throttle", "limitType"?, "limitedUntil", "retryAt"?, "since"?, "probeIssue"?, "heldIssues", "reroutedIssues" }.limitedUntilis always present andnullunless the vendor published a reset time;retryAtis when the breaker half-opens (the reset, or the cooldown end). This is the AGENT backend, unrelated torateLimits(tracker API budget) andoutboxEntries[].rateLimitedUntil(tracker writes). Absent on older daemons.autoSwitches(CORE-055) — issues whose next run uses an automatic override:{ "identifier", "source": "automation" | "backend_fallback" | "unknown", "fromBackend"?, "fromProfile"?, "toBackend"?, "toProfile"?, "reason"?, "switchedAt"? }, sorted by identifier. It describes the next dispatch; the running session’s backend stays onrunning[].backend./api/v1/issuesrows carry the same object asautoSwitch, and an issue held by an open breaker hasineligibleReason: "backend_limited".pauseReasons—{ "<identifier>": "<reason>" }for currently paused issues:user_cancelled,user_dismissed_input,retries_exhaustedortransition_failed(treat unknown values as opaque). An issue paused by an older daemon without a recorded reason is absent. Omitted when empty.failureAcks(CORE-175) —[{ "identifier", "upTo" }], sorted by identifier: the operator’s acknowledgements of worker failures (seePOST /issues/{identifier}/failures/ack). Omitted when empty.capabilities— optional daemon features the dashboard may use; currently["failure_ack"]. Treat unknown entries as opaque.totals(CORE-091) — daemon-session cumulative, reset on restart:{ "inputTokens", "outputTokens", "costUsdEstimated", "costCoverage": { "claudeRuns", "codexRuns" } }.costUsdEstimatedis the sum of Claude’s client-sidetotal_cost_usdestimates — counted per agent session, so a resumed session is not counted twice — and isnulluntil a Claude run reports a cost. Codex reports no cost:codexRuns > 0means the estimate covers Claude runs only.recentFailures— always an array (possibly empty) on a current daemon: the last 100 operator-relevant failures, oldest recorded first. Each row is{ "kind", "identifier"?, "source"?, "message", "occurredAt", "recordedAt", "count" }.kindisworker_failed,worker_stalled,tracker_poll,tracker_write,persist,outbox,panic,clientorautomation(an automation that could not be dispatched, e.g.pr_mergedafter a merge) (treat unknown kinds as opaque).messageis built from the classified error only (never prompt text or agent output), redacted, and capped at 1 KiB. Identical consecutive failures coalesce into one row with acount.occurredAtis when the failure happened (latest repeat),recordedAtwhen the daemon recorded it; an off-loop failure can be recorded after a newer one, so sort byoccurredAtto display. Rate-limited polls and outbox rate-limit deferrals are not failures. The list survives aWORKFLOW.mdreload but not a daemon restart.
Retry rows’ error and the rest of an agent failure’s text are passed
through the log redactor before they reach the snapshot: known key shapes,
NAME=value assignments whose upper-case name contains SECRET, TOKEN,
PASSWORD, API_KEY, PRIVATE_KEY, ACCESS_KEY or CREDENTIAL, the same
with a lower- or mixed-case name (password=, api_key=, "apiKey":,
x-api-key:), HTTP Basic credentials, URL passwords (also with an empty
user), and long random-looking tokens (also inside a path) appear as ***.
Vendor request ids (req_…, msg_…), SRI hashes, git SHAs, UUIDs and
ordinary paths stay readable.
Client error reports
Section titled “Client error reports”POST /api/v1/client-errors
Section titled “POST /api/v1/client-errors”The dashboard reports its own production failures here — error-boundary
crashes, uncaught error / unhandledrejection events, and snapshot schema
drift — so they reach the daemon log, itervox_client_errors_total and
recentFailures (kind client). Authenticated like the rest of /api/v1
(bearer token; in allow_unauthenticated mode the CSRF guard and Host guard
apply).
{ "kind": "render", "message": "Cannot read properties of undefined", "route": "/settings", "stack": "..." }kind:render,error,unhandledrejectionorschema; any other value is recorded asother.messageis required.- The body is capped at 8 KiB including trailing bytes and must be exactly
one JSON object. The daemon redacts
message,routeandstackand bounds them (1 KiB, 256 B, 2 KiB) before logging. - Responses:
202 {"accepted":true};400 bad_request(malformed, empty message, trailing data);413 payload_too_large;429 rate_limited(a daemon-wide budget of 20 reports, refilling one per 3 s, withRetry-After);503 orchestrator_busywhen the event queue is full (the report is dropped, not queued). - The dashboard itself dedupes identical
{kind, message, route}reports for 60 s, sends at most 10 per minute per tab, never reports its own failed POST, and sends nothing while it is on the token or server-down screen.
Issues
Section titled “Issues”Listing and detail
Section titled “Listing and detail”| Method | Path | Response |
|---|---|---|
GET | /issues | TrackerIssue[] |
GET | /issues/{identifier} | TrackerIssue |
Lifecycle and control
Section titled “Lifecycle and control”| Method | Path | Request body | Success response | Notes |
|---|---|---|---|---|
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-review | — | 202 {"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 |
While the daemon is draining (see draining under /ready), resume,
reanalyze, ai-review and provide-input are refused with
409 {"code":"draining"}: nothing was queued; re-send once the daemon is
back.
Backend health
Section titled “Backend health”| Method | Path | Request body | Success response |
|---|---|---|---|
POST | /backend-health/clear | {"backend":"claude","host":""} | 202 {"queued":true,"backend":"claude","host":""} |
Closes one agent-backend circuit breaker (backendHealth[] on the
snapshot) when you know the backend is available again. host is empty for
local runs or the SSH worker host of a host-scoped breaker. The clear is
queued on the orchestrator’s event loop: 202 means queued — watch
/api/v1/state or the SSE stream for the row to turn healthy. Issues held
with backend_limited on that breaker are re-evaluated on the next dispatch
pass. Errors: 400 bad_request (field: "backend" unless claude or
codex; field: "host" for a malformed host), 503 event_queue_full
(retry), 501 not_implemented. Like every state-changing route it needs the
bearer token, or, in server.allow_unauthenticated mode, passes the
cross-origin guard. The dashboard offers the same action as Clear next to
a limited backend chip.
Per-issue overrides and human-input flow
Section titled “Per-issue overrides and human-input flow”| Method | Path | Request body | Success 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} |
POST | /issues/{identifier}/failures/ack | {"upTo":"2026-09-27T10:00:00Z"} | 202 {"queued":true} |
POST | /issues/{identifier}/comment | {"body":"..."} | 202 {"queued":true,"identifier":"ENG-1"} or 200 {"ok":true,"identifier":"ENG-1"} |
POST | /issues/{identifier}/deps-override | — | 202 {"identifier":"ENG-1","overridden":true} |
DELETE | /issues/{identifier}/deps-override | — | 202 {"identifier":"ENG-1","overridden":false} |
backend returns 400 bad_request for anything but claude, codex or "",
and 409 backend_pin_refused (with the resolver’s reason in message,
field: "backend") when the issue’s command runs the other backend — for
example a codex pin over a claude ... profile, which the dispatcher would
refuse. A refused pin is not stored. A pin applies from the next run and wins
over agent.backend_fallback.
failures/ack (CORE-175) acknowledges the issue’s worker_failed /
worker_stalled rows in recentFailures that occurred at or before upTo
(RFC 3339), so the dashboard’s attention inbox stops counting them; a newer
failure is not covered. 400 bad_request for a missing or unparsable upTo,
404 issue_not_found when the issue has no such row, 503 orchestrator_busy
(Retry-After: 1) when the event queue is full. The ack is applied by the
event loop (the latest upTo per issue wins), is allowed while the daemon
drains (it starts no work), and is not persisted: like recentFailures, it
does not survive a restart, and it is dropped once the issue’s failures leave
the ring. Offered only when capabilities includes failure_ack.
provide-input / dismiss-input return 404 not_found when the issue is not
currently in input_required. provide-input returns 409 inline_input_enabled
when agent.inline_input: true — the tracker is the only reply channel in that
mode. The 409 is checked first: in inline mode the route refuses regardless
of the request body or the issue’s state. The agent-action provide-input route
(POST /agent-actions/{identifier}/provide-input) is unaffected.
Operator comments are plain comments (no managed marker): they can fire
tracker_comment_added automations and are delivered through the write-ahead
outbox when it is enabled. To answer an input-required agent, use
provide-input. The body is required and capped at 10 KiB; an empty or
oversize body returns 400 bad_request with field: "body".
Inferred-dependency override
Section titled “Inferred-dependency override”POST dismisses the LLM-inferred dependency gating layer for one issue;
DELETE restores it. This is the only mutation surface behind the dashboard’s
Deps tab.
Two things to know:
- Tracker-declared blockers are unaffected.
issue.BlockedBystays a hard block regardless of an override — this endpoint only relaxes the inferred layer. 202means queued, not applied. The change is routed through the orchestrator event loop. PollGET /stateor watch the SSE stream to observe the resultingInferredDepsentry.
Unlike the sibling issue actions there is no “must already be in state X”
precondition, so a failure has exactly one cause: the orchestrator’s event
channel was full. That returns 503 deps_override_queue_full rather than
404, signalling retry rather than unknown identifier.
Snapshot endpoints
Section titled “Snapshot endpoints”| Method | Path | Response |
|---|---|---|
GET | /issues/{identifier}/logs | IssueLogEntry[] |
GET | /issues/{identifier}/sublogs | IssueLogEntry[] |
GET | /logs/identifiers | string[] |
Clear endpoints
Section titled “Clear endpoints”| Method | Path | Success 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}/logsreads the in-memory orchestrator log buffer./issues/{identifier}/sublogsreads persisted agent session logs and returns an empty array when no logs exist./logsis an SSE stream of the daemon log file, not a JSON endpoint.
Runtime settings
Section titled “Runtime settings”All settings endpoints persist back to WORKFLOW.md and apply the value in
memory. None of them reloads WORKFLOW.md, so saving a setting never touches
an in-flight agent turn — this now includes /settings/tracker/states, the
Linear project filter (PUT /projects/filter) and
POST /settings/models/refresh, which used to reload (CORE-160).
| Method | Path | Request body | Success 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} |
POST | /settings/deps-analysis-mode | {"mode":"auto" | "manual"} | {"ok":true,"mode":"auto"} |
DELETE | /workspaces | — | 202 {"ok":true} |
POST | /refresh | — | 202 {"queued":true,"queued_at":"..."} |
POST | /automations/{id}/test | {"identifier":"ENG-42"} (target issue identifier) | {"ok":true} |
Skills Inventory
Section titled “Skills Inventory”The skills endpoints expose the Settings -> Skills inventory and recommendation
surface. They use the same dashboard bearer token as the rest of /api/v1.
| Method | Path | Request body | Success response | Notes |
|---|---|---|---|---|
GET | /skills/inventory | — | Inventory | 503 inventory_unavailable before the first successful scan |
POST | /skills/scan | — | Inventory | Forces a fresh filesystem scan |
GET | /skills/issues | — | InventoryIssue[] | 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/analytics | — | AnalyticsSnapshot | Includes HasRuntimeEvidence; 503 analytics_unavailable only before analytics can be computed |
GET | /skills/analytics/recommendations | — | Recommendation[] | 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”Profiles
Section titled “Profiles”| Method | Path | Request body | Success 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.
Reviewer and models
Section titled “Reviewer and models”| Method | Path | Response |
|---|---|---|
GET | /settings/reviewer | {"profile":"reviewer","auto_review":true} |
PUT | /settings/reviewer | {"ok":true} |
GET | /settings/models | { "<backend>": [ModelOption] } |
POST | /settings/models/refresh | {"ok":true,"models":{ "<backend>": [ModelOption] }} — body {"backend":"claude" | "codex" | "all"} (default all); rewrites agent.available_models in WORKFLOW.md and the live model list, without a reload. 503 settings_reloading like the other settings writes |
PUT /settings/reviewer request body:
{ "profile": "reviewer", "auto_review": true }Automations
Section titled “Automations”| Method | Path | Request body | Success 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:
croninput_requiredtracker_comment_addedissue_entered_stateissue_moved_to_backlogrun_failedpr_opened— fires when a worker’s PR is detectedrate_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_idinvalid_croninvalid_timezoneinvalid_regexinvalid_trigger_typeinvalid_match_modeinvalid_limit
Dependency analysis
Section titled “Dependency analysis”Runs the LLM analyzer that populates the inferred-dependency sidecar. One job runs at a time.
| Method | Path | Response |
|---|---|---|
POST | /deps/analyze | 202 {"jobId":"..."} — returns the in-flight job’s ID if one is already running |
GET | /deps/analyze/{jobId} | DepsAnalyzeJobRow — status, chunk progress, issuesScanned |
DELETE | /deps/analyze/{jobId} | Cancels a running job; 404 if it already finished |
Status is one of running, succeeded, failed, or cancelled. Cancellation
is real rather than cosmetic: the job context threads into exec.CommandContext,
so cancelling kills the agent subprocess.
Jobs are bounded by agent.deps_analyzer_timeout_ms (default 10 minutes,
matching the dashboard’s poll deadline) and chunked at
agent.deps_analyzer_chunk_size issues per turn.
Outbox
Section titled “Outbox”The write-ahead outbox holds tracker writes that have not yet been confirmed.
Enabled by default via tracker.outbox.
| Method | Path | Response |
|---|---|---|
POST | /outbox/{id}/retry | Re-queues a failed or degraded entry |
DELETE | /outbox/{id} | Discards an entry permanently |
Discard is the operator remedy for a stuck entry: an entry enqueued with no observed from-state baseline (currently only the issue-discard path) is exempt from supersede-reconciliation and will not clear on its own.
Pending entries appear on the state snapshot as outboxEntries[], each with
id, kind, identifier, targetState, attempts, lastError, degraded,
enqueuedAt, nextAttemptAt, and — when the last attempt was deferred by a
tracker rate limit — rateLimitedUntil (RFC 3339). Rate-limit deferrals do not
increase attempts and never set degraded.
Projects (Linear only)
Section titled “Projects (Linear only)”These endpoints return 501 not_supported for trackers without project support.
| Method | Path | Request body | Success 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
slugsresets to theWORKFLOW.mddefault.
Agent actions
Section titled “Agent actions”These routes are intended for agent subprocesses that have been granted
daemon-backed permissions through profile allowed_actions.
| Method | Path | Request body | Success 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:
| Variable | Description |
|---|---|
ITERVOX_ACTION_TOKEN | The short-lived per-run action grant. Pass as Authorization: Bearer $ITERVOX_ACTION_TOKEN on every /agent-actions/... call. |
ITERVOX_DAEMON_URL | The 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_IDENTIFIER | The issue this run belongs to (e.g. ENG-42). |
ITERVOX_CREATE_ISSUE_STATE | Set 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_ID | The 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:
unauthorizedagent_action_deniednot_supported
Core response shapes
Section titled “Core response shapes”ProfileDef
Section titled “ProfileDef”{ "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.
AutomationDef
Section titled “AutomationDef”{ "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 }}TrackerIssue
Section titled “TrackerIssue”Returned by both GET /issues and GET /issues/{identifier}.
Important fields include:
orchestratorState:idle,running,retrying,paused,input_required,pending_input_resumeagentProfile,agentBackendcomments,labels,branchName,blockedByblockedByDetails: richer blocker metadata for UI/API clients. Each entry hasidentifierand may includestateandurl.blockedByremains the compatibility string-array form.
IssueLogEntry
Section titled “IssueLogEntry”{ "level": "INFO", "event": "action", "message": "Write — updated README.md", "tool": "Write", "detail": "{\"status\":\"completed\",\"exit_code\":0}", "time": "12:34:56", "sessionId": "abc123"}Environment variables
Section titled “Environment variables”| Variable | Purpose |
|---|---|
ITERVOX_API_TOKEN | Explicit bearer token for non-loopback API access. |
LINEAR_API_KEY | Linear API token referenced from WORKFLOW.md. |
GITHUB_TOKEN | GitHub API token referenced from WORKFLOW.md. |
ITERVOX_DRY_RUN | Set to 1 to run the orchestrator without dispatching worker subprocesses. |