HTTP API Reference

All Cacophony services expose their APIs under /api/v1/. Default content type is application/json; SSE endpoints use text/event-stream.

Authentication

Prefer first-party caco CLI commands for local inspection because they read the node token without placing it in shell history or process arguments.

caco status --json true
caco node show --json true

If you must use raw HTTP on a trusted single-user machine, keep the token file private and avoid pasting bearer values into shared logs. Shell-expanded curl bearer-header examples can expose the token through process inspection while the command runs.

Discovery

MethodEndpointDescription
GET/api/v1/nodeNode identity, version, config hash, listeners, and startup-progress status
GET/api/v1/nodesAll configured nodes with last-seen, hash, liveness
GET/api/v1/nodes/<node>Single node detail
GET/api/v1/projectsAll configured projects
GET/api/v1/projects/<project>Single project detail

Cluster stats

MethodEndpointDescription
GET/api/v1/statsCanonical unified-stats aggregate consumed by all four CLI/web/Android/TUI frontends — one endpoint, not four parallel collectors. Query params: project (or all, default all), node (narrows the per-node rows; the fleet rollup stays whole), window (e.g. 24h/7d/2w or a bare hour count, default 24h), bucket (hour/day).

The /api/v1/stats response is { generated_at, scope{project?, node?, window_hours, bucket}, compute, per_node[], throughput, tokens }. compute is the cluster ClusterComputeRollup (nodes_counted, cpu_cores, mem_total_bytes, mem_used_bytes, disk_total_bytes, disk_used_bytes, nodes_with_disk) and each per_node[] row carries node, platform, load_avg_1, cpu_cores, and the same mem/disk byte fields, sourced from local live telemetry plus peer snapshots. tokens is the fleet token-usage rollup by project ({ by_project, grand_total }) summed across the local node and peers. throughput is the bead-lifecycle throughput summary by project: totals aggregates closed_last_24h/closed_last_7d and throughput_beads_per_hour, and each by_project[] entry adds throughput_window_hours, average_time_to_close_secs, and a series object — the dense closed-bead time-series for charts (window_hours, bucket, buckets, total_closed), driven by the request window/bucket. The whole field is null only when no beads data is reachable (for example when the beads primary is unavailable). Read-only; available on both the local and cluster listeners.

Feed

MethodEndpointDescription
GET/api/v1/feedFeed event history
GET/api/v1/feed/streamFeed SSE stream
GET/api/v1/ui/streamUI aggregate SSE stream for browser dashboard, companion app, and TUI (agents, beads, feed, choices, services)
GET/api/v1/ui/snapshotFull UI state snapshot for browser dashboard, companion app, and TUI hydration; includes crons[] with configured daemon cron entries used by mobile clients to enumerate cron names, speech history entries whose completed transcriptions include transcript text plus text_length, and a bounded topology projection with static/dynamic nodes, direct/relay edges, active beads-primary metadata, intended-primary views, and truncation markers for large clusters. ?exclude=<section,...> omits top-level data sections a client renders without (for example flow_definitions,flow_runs for a single-agent view); filtered bodies carry their own ETag and are cached per snapshot generation
GET/api/v1/operator-inboxOperator inbox messages (direct, broadcast, speech)

Browser caco-web treats HTTP 5xx snapshot/bootstrap failures and SSE/network failures that outlast the bounded 15s transient-error grace after the last healthy SSE open or UI event as Backend unavailable, even if the dashboard web service and local /health endpoint are still healthy during daemon restart convergence.

Daemon automation

MethodEndpointDescription
GET/api/v1/cronList configured daemon cron entries. Response data.crons[] includes name, schedule, action_type, command, and nodes.
POST/api/v1/cron/runDispatch a named cron immediately on the current node; body is {"name":"..."}. The caller should supply a stable x-caco-request-id: exact-ID retries replay one terminal receipt instead of executing twice, while a different request is refused during an in-flight run of the same cron. Success is reported only after observed command completion. Failures include the execution receipt; an outer deadline returns 503 cron_run_backpressured with request ID, phase, operation_started, dispatched:false, and Retry-After. The request deadline defaults to 330 seconds and can be overridden with CACO_CRON_RUN_REQUEST_TIMEOUT_SECS.
GET/api/v1/cron/logs?name=<name>&tail=<n>Tail one cron log file. Response data contains name, completed runs[], backward-compatible raw lines[], and log_path. Each run includes run_id, start/finish timestamps, duration, exit code, success/status, combined stdout/stderr output, and output_truncated. Combined output is response-bounded and preserves emission order because cron stderr is merged into stdout before capture. Android requests tail=80 for each distinct cron name from /api/v1/ui/snapshot.

Managed-agent Heartbeats

MethodEndpointDescription
GET/api/v1/heartbeatList local managed-agent heartbeat rows, or query agent/agent_id to resolve one local or remote owner and return its authoritative row over the daemon mesh. node remains a legacy local assertion for selector-free operations.
POST/api/v1/heartbeat/onPersist a runtime override enabling heartbeat delivery. Body {"agent":"..."} or {"agent_id":"..."} transparently routes to a remote owning daemon; an empty selector retains local-node bulk behavior.
POST/api/v1/heartbeat/offPersist a runtime override disabling heartbeat delivery when runtime opt-out is allowed, with the same selected-agent mesh routing and selector-free local bulk semantics.

Heartbeat APIs are authenticated mesh-routed controls for a selected agent: the ingress daemon resolves the authoritative owner and forwards over mTLS without requiring callers to know a node. Selector-free calls remain local-node bulk operations, and explicit CLI routing (caco @node heartbeat ... or caco @all heartbeat ...) remains available for deliberate node/fleet scopes. Status is readable by authenticated agent scopes; mutation requires operator/node or cluster-controller authority. JSON rows distinguish owner node, config defaults, runtime overrides, effective enabled state and reason, interval, last/next timestamps, delivery health, and transition provenance.

Feed Event Schema

{
  "id": "evt-01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "ts": "2026-03-16T12:00:00Z",
  "origin_node": "localhost",
  "project": "cacophony",
  "type": "agent_status",
  "sender": "localhost:cacophony:sunny-owl",
  "target": null,
  "payload": {
    "status": "running tests"
  }
}

Standard Event Types

Daemon Events

  • daemon_started
  • daemon_stopped
  • daemon_state

Config Events

  • config_hash
  • config_mismatch

Agent Events

  • agent_requested
  • agent_started
  • agent_status
  • agent_message
  • agent_broadcast
  • agent_completed
  • agent_failed
  • agent_stopped

Bead Events

  • bead_created
  • bead_updated
  • bead_claimed
  • bead_closed

Agents

MethodEndpointDescription
POST/api/v1/projects/<p>/agentsSpawn agent
GET/api/v1/projects/<p>/agentsList latest-known project agent inventory, including peer snapshot rows when available; ?local=true restricts to daemon-local live rows and responses include inventory/source metadata
GET/api/v1/projects/<p>/agents/<id>Project-validated agent detail
GET/api/v1/agents/<id>Compatibility detail alias for /api/v1/agents/<id>/status, including remote-owner forwarding
GET/api/v1/agents/<id>/launch?node=<owner>Shared launch progress: stage is one of accepted, project_checkout, workspace, configuring, launching, ready or failed, with an operator-facing label, per-stage history and a structured error on failure. Async creates return it as launch plus a launch_url; the optional node hint forwards cross-node launches before the agent row replicates. Every stage is also published as a launch_progress UI-stream event. 404 launch_unknown when nothing is tracked.
POST/api/v1/agents/<id>/stopStop agent (global; forwards to the owning node)
POST/api/v1/agents/<id>/pausePause agent (global; forwards to the owning node)
POST/api/v1/agents/<id>/restartRestart agent (global)
POST/api/v1/agents/<id>/resumeResume a failed/stopped/paused agent with a lifecycle-specific budget; client transport failures before a semantic response are surfaced as unexpected agent_resume_transport_error when the daemon node probe remains reachable.
POST/api/v1/agents/<id>/refreshRe-materialize the owning agent runtime config workspace in place. The response reports agent_dir, checkout_path, and updated_paths[]; session history, checkout data, and lifecycle state are preserved.
POST/api/v1/agents/<id>/nudgeNudge agent; lifecycle-scoped but intentionally shorter-budget than stop/restart/resume so controller health probes fail quickly with retryable diagnostics when tmux or peer forwarding is stuck
GET/api/v1/agents/summaryAggregate fleet health from the background agent read model. Potentially-stuck rows include bead context from the agent record or, when missing, from assigned in-progress bead lookup; JSON fields include bead_id_source, bead_status, stale_closed_bead_assignment, stale_closed_bead_id, bead_is_blocked, and bead_open_dependency_ids. Assignment, planned-outage, peer-health, and message-delivery-health collectors are independently bounded; base counts remain HTTP 200 while degraded, degraded_sections, collection_elapsed_ms, and section_timeout_ms identify omitted enrichment.
GET/api/v1/agents/<id>/statusCanonical global agent status/detail endpoint with remote-owner forwarding.
GET/api/v1/agents/<id>/logs / /log / /diffRead-only agent diagnostics: structured logs, raw tmux scrollback, and checkout diff. Diff supports summary_only=true and max_diff_bytes=<N> for lighter polling.
GET/api/v1/agents/<id>/files/tree / /files/readRead-only agent filesystem browser for managed agent directories.
GET/api/v1/agents/<id>/artefacts / /artefacts/<kind>/<index>Session summary and related agent artefact listing/reads.
GET/api/v1/agents/<id>/ptyCurrent structured interactive PTY WebSocket.
POST/api/v1/agents/<id>/{stop,restart,refresh,fork,pause,resume,discard,recreate,nudge}Global lifecycle controls that resolve and forward to the owning node.

The project-scoped agent endpoints validate project membership and are the preferred create/message surface. The global /api/v1/agents/... family is the cross-node read/lifecycle surface used by CLI, TUI, web, and companion clients. Do not call a non-existent /api/v1/agents/<id>/reintegrate; reintegration remains a lifecycle/CLI surface unless a future daemon endpoint is explicitly added.

Remote agent inspection and lifecycle forwards should not leak peer-side 502 Bad Gateway responses as recurring local daemon 502s. For common operator endpoints such as agent detail/status, restart, and diff, the local daemon classifies a forwarded unavailable target as remote_agent_unavailable with degraded/unavailable local status, remote node, endpoint, forwarded status, remote error code/message, and retryable metadata when available. Remote status forwarding is also bounded as an authority/health probe, so a settling or half-reachable peer returns structured retryable failure quickly instead of consuming an entire controller sweep.

Spawn Request Body

{
  "node": "localhost",
  "type": "codex",
  "profile": "worker",
  "goal": "Implement ready work",
  "id": null,
  "env": {},
  "hooks": {
    "on_start": [],
    "before_tool": [],
    "after_tool": [],
    "on_complete": []
  },
  "reintegration": {
    "mode": "direct"
  }
}

Session summaries

Summary APIs return the union of state-branch .cacophony/agent/.../summary records and legacy default-branch summaries, de-duplicating by project, agent, and index with the state-branch copy preferred. For state-branch reads, the daemon prefers a warmed read-only checkout under $CACOPHONY_DIR/state-branches/<project>/ and falls back to the canonical project checkout's remote-tracking state ref when that cache is unavailable.

MethodEndpointDescription
GET/api/v1/summaries?project=&agent_id=&bead_id=&limit=&offset=List recorded summaries, newest first. project may be omitted, a single project, or a comma-separated project list. Responses include items, total, limit, and offset for paging; large unscoped state branches use a bounded recent-commit fast path before falling back to exhaustive enumeration, while agent_id-filtered state-branch walks stay scoped to that agent's summary subtree. List responses may also include first_detail, the parsed detail for the first returned row, so TUI, web, and Android summary viewers can populate the initial detail pane without a second request while still falling back to the detail endpoint when absent. Summary endpoints use a dedicated daemon timeout (default 60s, override CACO_SUMMARIES_REQUEST_TIMEOUT_SECS, minimum 5s) so cold state-branch scans do not fail under the ordinary control-plane budget. CLI summary readers distinguish a whole-daemon transport outage from a summaries-only failure; if /api/v1/node is healthy but the summaries route fails, JSON reports retryable summaries_unavailable instead of telling operators to run caco up.
GET/api/v1/summaries/<agent_id>/<index>?project=Show one parsed summary with Goal / Beads / Before / After / Diff / Artefacts / Takeaway sections.
GET/api/v1/summaries/<agent_id>/<index>/raw/<asset_path>?project=Serve a recorded raw artefact such as a screenshot, terminal cast, or data.json. Paths are constrained to the selected summary directory.

Beads

MethodEndpointDescription
POST/api/v1/projects/<p>/beadsCreate bead
GET/api/v1/projects/<p>/beadsList beads (with query params)
GET/api/v1/projects/<p>/beads/<id>Bead detail
PATCH/api/v1/projects/<p>/beads/<id>Update bead
DELETE/api/v1/projects/<p>/beads/<id>Delete bead
POST/api/v1/projects/<p>/beads/refineApply post-creation refinement instructions to selected existing beads
POST/api/v1/projects/<p>/beads/dispatchAtomically reserve one bead and spawn or queue its exact worker. JSON fallback and SSE use the shared typed phase contract; terminal outcomes are done, queued, or failed. The queued receipt carries bead, reserved agent, node, queue-entry id, and position.
POST/api/v1/projects/<p>/beads/claimClaim next ready bead
POST/api/v1/projects/<p>/beads/<id>/closeClose bead
GET/api/v1/projects/<p>/beads/<id>/attachmentsList bead attachments
POST/api/v1/projects/<p>/beads/<id>/attachmentsAppend bead attachments
GET/api/v1/projects/<p>/beads/<id>/attachments/<attachment_id>Read one bead-authorized attachment
GET/api/v1/projects/<p>/beads/statsLifecycle metrics (avg time-to-close, throughput, in-progress)
GET/api/v1/projects/<p>/beads/throughput-series?window_hours=<h>&bucket=<hour|day>Closed-bead throughput time-series buckets — the data behind the stats throughput view. The unified /api/v1/stats throughput field carries the matching by-project closed-throughput summary.
GET/api/v1/beads/allGlobal bead list with owning project per row
GET/api/v1/beads/spoken-namesLightweight cross-project TTS projection with only project, id, and spoken_name rows plus count/failure metadata
GET/api/v1/beadsCanonical beads routing and status summary, including the primary view that caco status and caco bd status use for authority identity

Messaging

MethodEndpointDescription
POST/api/v1/projects/<p>/messages/sendSend direct message; accepts only after bounded local durable storage and returns retryable msg_send_backpressured when the message store is unavailable before acceptance; replication, injection, audit/event-log append, and lifecycle refresh continue after acceptance; the CLI performs one mutating POST attempt and classifies transport failures as msg_send_backpressured with endpoint/node-probe/retry metadata when /api/v1/node remains healthy; if the bounded node probe is unavailable too, the same retryable classification is returned with daemon_probe.ok:false instead of retrying a potentially accepted send
POST/api/v1/projects/<p>/nodes/<node>/messages/sendFan out ordinary direct-message rows to injectable agents on one node; request global:true includes all projects on that node, and responses include per-recipient accepted/failed metadata
POST/api/v1/projects/<p>/messages/broadcastBroadcast message
POST/api/v1/projects/<p>/messages/speakSpeak via TTS. A busy primary message store returns bounded HTTP 503 message_store_busy with Retry-After, lock-wait/timeout diagnostics, and operation_started:false.
GET/api/v1/projects/<p>/messages/inboxRead inbox
GET/api/v1/projects/<p>/messages/chatProject chat history (?limit=, ?include_body=, ?body_preview=, ?before=). before is an RFC3339 cursor returning only strictly older messages, so memory-bounded clients page backwards instead of refetching a larger full-history response; a short page signals end of history.
GET/api/v1/messages/chatGlobal chat history (all projects); same limit/include_body/body_preview/before parameters
POST/api/v1/messages/broadcastBroadcast message (global). Direct send and speak are project-scoped only; there is no global /api/v1/messages/send or /speak.

Actions

MethodEndpointDescription
GET/api/v1/actionsList available project actions
POST/api/v1/actions/{name}/runExecute a project action

Release Queue

MethodEndpointDescription
POST/api/v1/projects/<p>/releasesTrigger a release job
GET/api/v1/projects/<p>/releasesList release jobs (?status=, ?channel=, ?limit=). Terminal status includes completed, skipped, failed, canceled, and error. Provider-backed rows expose provider_conclusion; a provider-confirmed no-op additionally exposes no_op_reason and uses state: "skipped" rather than a failure state. Owner-dispatched Android Play promotion rows use deterministic android-promotion-<source-run-id> identities and expose exact source_run_id, source_head_sha, and downstream dispatched_run_id correlation through the same durable/replicated release surface.
GET/api/v1/projects/<p>/releases/{id}Release job detail
GET/api/v1/projects/<p>/releases/{id}/logsRelease job build/push output
POST/api/v1/projects/<p>/releases/{id}/cancelCancel a release job
POST/api/v1/projects/<p>/releases/syncTrigger GitHub Actions sync
GET/api/v1/projects/<p>/releases/configResolved release channel configuration

Notifications

MethodEndpointDescription
POST/api/v1/notificationsCreate a replicated operator notification from caco notify send or a first-party subsystem.
GET/api/v1/notifications?project=&level=&source=&acknowledged=&limit=List recent notifications, newest first, with optional project, severity, source, acknowledgement-state, and limit filters.
GET/api/v1/notifications/{notif_id}Fetch one notification by ID.
POST/api/v1/notifications/{notif_id}/ackAcknowledge one notification. Web and TUI clear/read actions call this endpoint so dismissed rows stay dismissed after the next snapshot refresh.

Suggestions

MethodEndpointDescription
POST/api/v1/suggestGenerate a suggestion set from a prompt (node-local; proposes only, never runs).
GET/api/v1/suggest/listList suggestion sets.
GET/api/v1/suggest/{uuid}Fetch one suggestion set, including option run-state.
POST/api/v1/suggest/{uuid}/{option_uuid}/runRun one option, node-local, under the high-risk DENY backstop plus run-once enforcement (403 suggest_run_denied_high_risk / 409 suggest_run_already_run).

File cache

MethodEndpointDescription
GET / POST/api/v1/file-cacheList cached project files, or add a file/content blob.
GET/api/v1/file-cache/{file_id}/contentRetrieve file bytes. Internal cluster-mTLS content URLs are sanitized to null in user-facing records.
GET/api/v1/file-cache/{file_id}/previewBounded preview of a cached file.
GET/api/v1/file-cache/{file_id}/associationsAssociated bead/agent/build/test IDs for a file.

Links

MethodEndpointDescription
GET / POST/api/v1/linksList URL/link artifacts, or record one, with project/bead/tag/host filters. Internal cluster-API URLs are refused at the writer and sanitized in user-facing fields.

Audio

MethodEndpointDescription
POST/api/v1/audio/speechText-to-speech
POST/api/v1/audio/transcriptionSpeech-to-text through provider, local, or configured command-template STT backends; command-template model labels advertised by capabilities are valid model values. Provider warmup/backpressure/connectivity failures return structured transcription_unavailable diagnostics such as transcription_provider_unavailable with retry metadata instead of opaque gateway errors.

TTS Request

{
  "model": "gpt-4o-mini-tts",
  "voice": "sage",
  "text": "Build completed successfully",
  "format": "wav"
}

Supported TTS models: gpt-4o-mini-tts, azure/speech/azure-tts, gemini-3.1-flash-tts-preview. Gemini TTS uses the generateContent API with response_modalities: ["AUDIO"] and requires a Google API key.

TTS failures use classified provider error envelopes instead of a generic timeout. Error codes include upstream_provider_5xx, upstream_provider_4xx, upstream_provider_timeout, upstream_provider_unreachable, upstream_provider_transport_error, and upstream_response_read_failed. Where available, details includes upstream_error_class, provider host/endpoint, and HTTP status so callers can distinguish provider outage, bad request, timeout, and transport failures.

Config

MethodEndpointDescription
GET/api/v1/config/hashLocal config hash
GET/api/v1/config/peersPeer config hashes
GET/api/v1/config/diffConfig comparison

Choices & Operator Inbox

MethodEndpointDescription
GET/api/v1/choicesCurrent active choices (excludes test-agent choices). Read responses include choice_id, choiceId, and legacy id aliases for the same interaction. A busy persisted store returns bounded HTTP 503 choices_store_backpressured with state_known_clear:false, never a fabricated empty set.
GET/api/v1/choices/listChoice history with optional status/project filters. Read responses include choice_id, choiceId, and legacy id aliases.
GET/api/v1/choices/{choice_id}Single choice lookup by interaction ID, including terminal choices. Read responses include choice_id, choiceId, and legacy id aliases.
POST/api/v1/choices/presentPresent a new choice to the operator. Store-lock contention returns bounded retryable choices_present_backpressured with operation_started:false before any insert.
POST/api/v1/choices/resolveResolve an active choice (first-writer-wins)

Active unresolved choices are treated as urgent operator-decision blockers across first-party clients. TUI, caco-web, Android, macOS, and speech/readout surfaces derive their pending-choice counts from these choices/inbox APIs, route the operator to a resolution surface, and clear the urgent state once the shared choice status becomes terminal. Replicated choice_presented and choice_resolved feed events also materialize or update the durable local operator-inbox choice row so remote choice reads and operator surfaces do not wait for a slower full-state inbox pass before showing or clearing urgent decisions.

Test Agent Isolation The /api/v1/choices endpoint automatically filters out choices from test-type agents, preventing test data from leaking into operator-facing surfaces.

Restart

MethodEndpointDescription
POST/api/v1/restartTrigger daemon restart (fire-and-forget)

The restart endpoint responds immediately with accepted: true before spawning the restart subprocess. Since the restart tears down the current daemon process, the actual outcome cannot be reported synchronously. The caller should poll daemon health to determine success. Available on both local and cluster listeners.

Logs

MethodEndpointDescription
GET/api/v1/logsLog history; mobile clients should request and render bounded tails
GET/api/v1/logs/streamLog SSE stream
POST/api/v1/logs/errorSubmit a structured error report (handle_log_error). Companion apps post runtime errors as first-class platform errors using their existing node/operator token, tagged via component (ios-app / android-app) plus platform: / surface: / kind: labels; see the Logs error-report contract.

Sidecar Endpoints

Each long-running service exposes a sidecar with uniform endpoints:

EndpointPurpose
GET /pingLiveness check
GET /healthHealth with check details
GET /logsLog metadata and tailing

Cluster-Only Endpoints

These endpoints are available only on the cluster (HTTPS/mTLS) listener:

MethodEndpointDescription
POST/api/v1/feed/eventsAccept feed events from peer