API Reference
This is the reference for Joe’s HTTP API: every operator- and user-facing endpoint, the method and path it answers on, the authentication it requires, and the request and response shapes it actually reads and returns. It is a lookup surface. For why a mechanism exists see Concepts; for the procedure that uses an endpoint see Guides.
Base path and transport
All API endpoints live under /api/v1 and are served on the daemon’s API port (default
7777, set by server.address — see Configuration). Requests and
responses are JSON unless noted. The Prometheus /metrics endpoint is not part of
this API — it is served on a separate metrics port and is documented under
Operations.
Authentication tiers
Every endpoint falls into one of three tiers. The tier is named in each entry below.
- No-auth — reachable without a credential. Only the paths under
/api/v1/auth/are public. - Authenticated — requires a resolved principal: a human session cookie or a
service-account bearer key. Unauthenticated requests on these paths receive
401. The daemon refuses to start without an identity configuration, so in a running deployment every non-public request carries a principal. See Principals and identity. - Admin-gated — additionally requires that the principal hold the admin capability.
Non-admins receive
403. See RBAC, zones, and read posture.
A human presents its session via the joe_session cookie set by the login flow; a
machine presents its key as Authorization: Bearer <key>.
Component-scoped reads pass through the read-posture and RBAC layer in addition to the
tier above. In the default team_flat posture every authenticated principal reads every
component; under zoned a read requires a grant. Mutating a component is governed
independently by RBAC and the write floor regardless of read posture — see
RBAC, zones, and read posture and
Observation mode and the write floor.
Conventions
- Path parameters are written
{name}and substituted into the path. - Errors are returned as
{"error": "<code>", "message": "<text>", "details": {…}}with the matching HTTP status;detailsis present only when the handler supplies it. - Timestamps are RFC 3339 strings.
Status and version
GET /api/v1/status
The liveness surface. Returns the running status, the build version, and the server time. Auth: authenticated.
Response 200:
{ "status": "ok", "version": "v1.2.3", "time": "2026-06-28T12:00:00Z" }There is no separate /livez, /readyz, or /healthz endpoint; this is the liveness
surface.
GET /api/v1/version
Returns the full build-identity record. Auth: authenticated.
Response 200:
{
"version": "v1.2.3",
"commit": "abc1234",
"build_time": "2026-06-28T00:00:00Z",
"ui_digest": "sha256:…"
}ui_digest is a sha256 over the embedded UI bytes the binary serves; /status does not
carry it.
Mutate status and panic
GET /api/v1/mutate-status
Reports whether Joe may currently mutate the managed system, and why, from the boot-resolved write floor. Auth: authenticated.
Response 200:
{ "can_mutate": false, "reason": "observation" }reason is one of full (floor down, mutates permitted subject to RBAC),
observation (booted into observation mode), or safe_mode (panic recovery). The
full value is part of the enum but is not currently returned — the governed
full-capabilities mode that brings the floor down is not yet implemented, so a shipped
Joe reports observation or safe_mode. See
Observation mode and the write floor.
POST /api/v1/panic
Triggers emergency shutdown: persists the panic state and schedules a process exit so the daemon restarts into safe mode. Auth: authenticated.
Request body (optional):
{ "reason": "string" }Response 200:
{ "acknowledged": true, "message": "emergency shutdown initiated — joe will restart in safe mode" }The call is idempotent — triggering while already panicked returns acknowledged: true
with a message noting the existing state. There is no HTTP endpoint to clear safe mode;
recovery is the joe unlock CLI plus a restart, documented under
Operations.
GET /api/v1/panic/status
Reports the current safe-mode status. Auth: authenticated.
Response 200 (not in safe mode):
{ "safe_mode": false }Response 200 (in safe mode), enriched from the persisted panic row when available:
{
"safe_mode": true,
"triggered_at": "2026-06-28T12:00:00Z",
"trigger_source": "api",
"trigger_reason": "string"
}Authentication
These endpoints are public (no-auth) and active only when OIDC is configured. See Principals and identity.
GET /api/v1/auth/config
Reports whether human (OIDC) login is available, so a client knows whether to offer the login button. Auth: no-auth.
Response 200:
{ "oidc_enabled": true }GET /api/v1/auth/login
Begins the OIDC login flow. Sets a short-lived state cookie and redirects (302) to the
identity provider. Auth: no-auth. No JSON body.
GET /api/v1/auth/callback
The OIDC redirect target. Reads state, code, and an optional error query
parameter, validates the flow, mints a session, sets the session cookie, and redirects
(302) to the post-login path. On failure it returns the standard JSON error shape.
Auth: no-auth.
POST /api/v1/auth/logout
Revokes the caller’s server-side session and clears the session cookie. Auth: no-auth (it acts on the presented cookie). No request body.
Response 200:
{ "status": "logged out" }GET /api/v1/me
Returns the resolved caller: the principal, whether it is an admin, whether RBAC and OIDC are active, and the zones the principal may act in. Auth: authenticated.
Response 200:
{
"principal": "user:alice@example.com",
"is_admin": true,
"rbac_enabled": true,
"oidc_enabled": true,
"zones": [ { "id": "prod-readonly", "allowed_actions": ["read"] } ]
}Sessions
Chat sessions are owned and lifecycle-managed. A non-owner’s access is read-only or
denied per the sharing rules in Chat sessions. All session
endpoints are authenticated; ownership is enforced inside the handler. A session
object (webUISession) carries:
{
"id": "string",
"started_at": "2026-06-28T12:00:00Z",
"last_activity_at": "2026-06-28T12:05:00Z",
"summary": "string",
"message_count": 12,
"title": "string",
"read_only": false,
"linked_incident_id": "string",
"linked_incident_title": "string",
"incident_involved": false,
"type": "string",
"incident_state": "string",
"shared_by": "string",
"creator_principal": "string",
"trashed_at": "2026-06-28T12:00:00Z",
"archived_at": "2026-06-28T12:00:00Z",
"purge_after": "2026-06-28T12:00:00Z"
}GET /api/v1/sessions
Lists sessions. Query parameters: limit (default 20), mine (true restricts to the
caller’s own). Response 200: { "sessions": [ … ], "count": N }.
POST /api/v1/sessions
Creates a new session owned by the caller. No request body. Response 201: the session
object.
GET /api/v1/sessions/trash
Lists the caller’s trashed sessions. Query parameter: limit (default 20). Response
200: { "sessions": [ … ], "count": N }.
GET /api/v1/sessions/{id}
Returns one session. Response 200: the session object. 404 if not found or not
visible to the caller.
GET /api/v1/sessions/{id}/messages
Returns the session’s messages. Response 200: { "messages": [ … ], "count": N },
where each message is:
{
"id": "string",
"session_id": "string",
"role": "user",
"content": "string",
"tool_name": "string",
"created_at": "2026-06-28T12:00:00Z"
}PATCH /api/v1/sessions/{id}
Renames a session. Request body { "title": "string" } (required, non-empty). Response
200: the updated session. 400 on an empty title, 404 if not owned.
DELETE /api/v1/sessions/{id}
Moves a session to trash. Response 204. 404 if not owned.
POST /api/v1/sessions/{id}/restore
Restores a trashed session. No request body. Response 200: the session. 409 if the
session is not in trash.
POST /api/v1/sessions/{id}/link-incident
Links the session to the active incident. No request body. Response 200: the session.
409 if there is no active incident or the link is not allowed. See
Incidents.
Tasks
A task runs the agent loop against a message and returns the transcript. Both endpoints
are authenticated; when session_id is supplied, session ownership applies. See
The agent loop and autonomy.
Request body (shared by both endpoints):
{
"message": "string",
"session_id": "string",
"config": {
"max_iterations": 10,
"safety_tier": "string",
"timeout": "30s",
"allowed_zones": ["string"],
"allowed_namespaces": ["string"],
"namespace_zones": { "namespace": "zone" }
}
}config and its fields are optional.
POST /api/v1/tasks
Runs the task to completion and returns the full result synchronously. Response 200:
{
"task_id": "string",
"session_id": "string",
"status": "string",
"iterations": 3,
"steps": [ { "step_number": 1, "llm_request": {…}, "llm_response": {…}, "tool_results": [ … ] } ],
"final_answer": "string",
"tools_used": ["string"],
"total_tokens": { "input_tokens": 100, "output_tokens": 50 },
"duration_ms": 1234,
"error": "string",
"error_code": "string"
}Each step carries the LLM request (message_count, tools_available), the LLM response
(content, optional tool_calls, usage), and any tool_results. The response also
surfaces context-management fields (history_trimmed, messages_dropped,
tool_results_truncated, user_message_truncated, context_window_tokens) when they
apply.
POST /api/v1/tasks/stream
Runs the task and streams progress as Server-Sent Events
(Content-Type: text/event-stream). Each loop iteration emits a step event whose data
is a task step; a terminal final event carries the same object as the synchronous
POST /api/v1/tasks response.
Components
A component is a registered external system. Listing and reading types is authenticated; registering, deleting, inspecting promotion, and promoting are admin-gated. Registration accepts no credential material — a component is armed later by promotion, which stores a credential reference (an env-var name), never a secret value. See The component lifecycle and Components.
GET /api/v1/component-types
Lists the supported component types. Auth: authenticated. Response 200:
{ "component_types": ["string"], "count": N }.
GET /api/v1/components
Lists registered components as a read-model projection. Auth: authenticated.
Response 200: { "components": [ … ], "count": N }, where each entry is:
{
"id": "string",
"type": "string",
"name": "string",
"armed": false,
"provider": "string",
"status": "string",
"last_sync_at": "2026-06-28T12:00:00Z",
"last_error": "string",
"created_at": "2026-06-28T12:00:00Z",
"updated_at": "2026-06-28T12:00:00Z"
}POST /api/v1/components
Registers a component. Auth: admin-gated. Request body:
{ "id": "string", "type": "string", "name": "string", "config": { } }Credential-bearing fields in config are rejected. Response 201: the stored component
(id, type, name, config, status, last_sync_at, last_error, created_at,
updated_at). 409 if the id exists, 400 if the type is unsupported.
GET /api/v1/components/{id}
Returns one component as the read-model projection (the same shape as the list entry; the
raw config is not echoed). Auth: authenticated. 404 if not found.
DELETE /api/v1/components/{id}
Removes a component and unregisters its adapter. Auth: admin-gated. Response 204.
GET /api/v1/components/{id}/promotion-requirements
Describes what arming this component’s type requires — the credential provider kind and
its locator fields and constraints — without contacting any backend. Auth:
admin-gated. Response 200 for a wired type:
{ "type": "string", "wired": true, "kind": "string", "locator_fields": [ … ], "constraints": [ … ] }For an unwired type the response is { "type": …, "wired": false, "armable_types": [ … ] }.
GET /api/v1/components/{id}/promotion-candidates
Lists the environment-variable names that could supply this component’s credential
reference, reflecting the live environment. Names only — never values. Auth:
admin-gated. Response 200 for a wired type:
{
"type": "string",
"wired": true,
"kind": "string",
"prefix": "JOE_<SEGMENT>_",
"applicable": true,
"candidates": [ { "label": "string", "env_var_name": "string" } ]
}For an unwired type the response mirrors promotion-requirements
({ "type": …, "wired": false, "armable_types": [ … ] }).
POST /api/v1/components/{id}/promote
Arms a component by attaching a credential reference. Auth: admin-gated. Request body (fields depend on the provider):
{
"credential_provider": "string",
"env_var": "string",
"value": "string",
"auth_method": "static-bearer | entra-exchange",
"api_server": "string",
"ca_data": "string",
"namespace": "string",
"in_cluster": false,
"tenant_id": "string",
"client_id": "string",
"client_secret_env_var": "string",
"audience": "string"
}For a static-credential component the reference is an env_var. For Kubernetes the
reference carries the cluster coordinates (api_server, ca_data, optional namespace)
plus a credential for one of two auth_method values:
static-bearer— a bearer-token source, either anenv_varname orin_cluster: true(the pod-mounted service-account token).entra-exchange(AKS) — Joe mints a short-lived bearer token via an Azure Entra OAuth2 client-credentials exchange, usingtenant_id,client_id,audience(the scope), andclient_secret_env_var(the variable holding the app registration’s client secret).client_secret_env_varis a distinct field from the static-bearerenv_varand is exempt from the env-var uniqueness guard, so one app registration’s client secret may be shared across clusters.
An inline value (an embedded secret) is refused; every reference must be an indirection.
Promotion resolves no credential. Response 200:
{ "component_id": "string", "type": "string", "provider": "string", "armed": true, "rearm": false }POST /api/v1/components/{id}/test
Tests connectivity for a registered component. Auth: authenticated. No request body.
Response 200: { "ok": true, "message": "string" }. 404 if the component is not
found.
Observe (category-based)
These endpoints answer a natural-language question against a category of telemetry. Joe resolves the backing component for the named service through the knowledge graph, translates the question to a native query, executes it, and returns a normalized result. All are authenticated (component reads are subject to read posture). See The knowledge graph and the Knowledge graph guide.
Shared request body:
{ "service": "string", "question": "string" }For metrics, logs, traces, and k8s, both fields are required; for alerts,
service is required and question is an optional filter. A 404 is returned when no
backing component for the service is found via the appropriate graph edge.
POST /api/v1/observe/metrics
POST /api/v1/observe/logs
POST /api/v1/observe/traces
Each returns a normalized observability result. Response 200:
{
"source": "string",
"component_id": "string",
"native_query": "string",
"data": [ { "timestamp": "2026-06-28T12:00:00Z", "labels": { }, "value": 0 } ],
"raw_result": { }
}POST /api/v1/observe/alerts
Resolves the alerts (or paging) backend for the service and returns its alerts. Response
200:
{
"source": "string",
"component_id": "string",
"alerts": [ { "name": "string", "state": "string", "labels": { }, "summary": "string" } ],
"count": 0
}POST /api/v1/observe/k8s
Resolves the Kubernetes backend for the service and returns pods (or pod logs when the
question mentions logs). Response 200:
{ "source": "kubernetes", "component_id": "string", "native_query": "string", "data": { } }Knowledge graph
The graph query endpoints are authenticated. The knowledge graph models infrastructure relationships; see The knowledge graph.
GET /api/v1/graph/query
Searches for nodes. Query parameter: q (required). Response 200:
{ "nodes": [ … ], "count": N }, where each node carries ID, Type, ComponentID,
Metadata, FirstSeen, LastSeen. 400 if q is missing.
GET /api/v1/graph/related
Returns the subgraph around a node. Query parameters: nodeID (required), depth
(optional, default 1). Response 200: a subgraph { "Nodes": [ … ], "Edges": [ … ] };
each edge carries From, To, Relation, Confidence, Source, ComponentID,
Context, CreatedAt. 404 if the node is not found.
GET /api/v1/graph/summary
Returns graph totals. Response 200: { "NodeCount", "EdgeCount", "NodesByType", "RecentlyAdded", "RecentlyUpdated" }.
Web-UI graph views
The web UI consumes a presentation-shaped view of the same graph. These are authenticated.
GET /api/v1/graph— the full graph as{ "nodes": [ … ], "edges": [ … ] }, where a node carriesid,kind,name,namespace,cluster,metadata,labels,statusand an edge carriesid,source,target,type.GET /api/v1/graph/node/{id}— one node in the same node shape;404if not found.GET /api/v1/graph/node/{id}/related—{ "nodes", "edges" }around the node; query parameterdepth(default 1).
Incident regime
These endpoints read and change the install-wide incident regime and a session’s
incident state. Reading the regime is authenticated. Declaring, resolving, promoting,
and advancing state require the principal to hold access to the regime-control zone —
authorization is enforced inside the handler and refused with 403; they also return
503 when RBAC is not configured. See The incident regime
and Incidents.
GET /api/v1/regime
Returns the current regime, enriched with the active incident’s session id, state, and
title when one is active. Response 200:
{
"Mode": "normal",
"DeclaredAt": "2026-06-28T12:00:00Z",
"DeclaredByPrincipal": "user:alice@example.com",
"DeclaredKind": "human",
"IncidentSessionID": "string",
"IncidentState": "string",
"IncidentTitle": "string"
}POST /api/v1/regime/declare
Declares an incident regime and makes the declaring human captain of the named session.
Request body { "session_id": "string", "declared_kind": "human" } (session_id
required; declared_kind defaults to human — joe is refused with 403). Response
201: { "session_id": "string", "captain_id": "string", "declared_by": "string" }.
409 if a regime or session is already an incident.
POST /api/v1/regime/resolve
Resolves the active incident regime. Optional body { "as_joe": false }. Response 200:
{ "session_id": "string", "resolved_by": "string" }. 409 if there is no active
incident or it is not yet mitigated.
POST /api/v1/sessions/{id}/promote-incident
Promotes an existing session to the incident master in place. Optional body
{ "declared_kind": "human" }. Response 201: same shape as declare. Same authorization
and conflict semantics as declare.
POST /api/v1/sessions/{id}/incident-state
Advances the incident’s state. Request body
{ "state": "being_worked" | "believed_mitigated" } (required). Response 200:
{ "session_id": "string", "incident_state": "string" }. 409 if the session is not an
incident or the incident is already terminal.
Admin surface
Every endpoint under /api/v1/admin/ is admin-gated: a non-admin principal receives
403 and the attempt is audited. These endpoints govern RBAC, identity, read posture,
and credential diagnostics. See
RBAC, zones, and read posture and
Principals and identity.
Zones
GET /api/v1/admin/zones—{ "zones": [ … ], "count": N }; a zone is{ "id", "name", "description", "allowed_actions", "created_at" }.POST /api/v1/admin/zones— body a zone (allowed_actionsdefaults to["read"]). Response201: the zone.PATCH /api/v1/admin/zones/{id}— partial update{ "name", "description", "allowed_actions" }. Response200: the zone.DELETE /api/v1/admin/zones/{id}—{ "status": "deleted", "id": "string" }.
Component-zone assignments
GET /api/v1/admin/component-zones—{ "assignments": [ … ], "count": N }; an assignment is{ "component_id", "zone_id", "assigned_by", "reason", "assigned_at" }.POST /api/v1/admin/component-zones— body{ "component_id", "zone_id", "assigned_by", "reason" }. Response200: the assignment.DELETE /api/v1/admin/component-zones/{componentID}—{ "component_id", "removed", "note" }; the component falls back to the default unassigned zone.
Policies
GET /api/v1/admin/policies—{ "policies": [ … ], "count": N }; a policy is{ "id", "principal", "zone_id", "created_at" }.POST /api/v1/admin/policies— body{ "principal", "zone_id" }(principalmust be prefixeduser:,group:, orsvc:). Response201: the policy.POST /api/v1/admin/policies/revoke— body{ "principal", "zone_id" }. Response200:{ "status": "revoked", "principal", "zone_id", "removed" }.DELETE /api/v1/admin/policies/{id}—{ "status": "deleted" }.GET /api/v1/admin/unassigned— components with no zone:{ "component_ids": ["string"], "count": N }.
Admin roster
GET /api/v1/admin/admins—{ "admins": [ … ], "count": N }; an entry is{ "principal", "granted_at", "granted_by", "reason" }.POST /api/v1/admin/admins— body{ "principal", "reason" }. Response200:{ "principal", "granted" }.DELETE /api/v1/admin/admins/{principal}—{ "principal", "removed" }.
Principals
GET /api/v1/admin/principals—{ "principals": [ … ], "count": N }; a record is{ "principal", "created_at", "status", "disabled_at", "disabled_by", "display_name", "last_seen_at" }.POST /api/v1/admin/principals/{principal}/disable—{ "principal", "status": "disabled", "sessions_revoked", "registry_modified" }.POST /api/v1/admin/principals/{principal}/enable—{ "principal", "status": "active" }.
Read posture and read promotions
GET /api/v1/admin/read-posture—{ "posture": "team_flat" | "zoned" }.POST /api/v1/admin/read-posture— body{ "posture": "team_flat" | "zoned" }. Response200:{ "posture": "string" }.GET /api/v1/admin/read-promotions—{ "read_promotions": [ { "component_type", "enabled" } ], "count": N }.POST /api/v1/admin/read-promotions— body{ "component_type", "enabled" }. Response200:{ "component_type", "enabled" }.
Credential status
GET /api/v1/admin/credential-status— credential descriptors for registered components, with no backend contact:{ "statuses": [ … ], "count": N }. A status is{ "component_id", "type", "name", "descriptor": { "provider", "audience", "context", "expires_at" }, "error" }.POST /api/v1/admin/credential-status/{componentID}/probe— performs a live resolve and probe:{ "component_id", "diagnostic": { "component_id", "provider", "audience", "expires_at", "stage", "ok", "reason" }, "stderr_available" }.POST /api/v1/admin/credential-status/{componentID}/probe/stderr— returns captured plugin stderr from the last probe:{ "component_id", "stderr" }.
Endpoints not documented individually
The following served endpoints exist but are outside the operator- and user-facing surface above. They are grouped here rather than documented one by one.
- Agent-runtime and captaincy surface — the persistence and coordination endpoints
the agent loop and captaincy transfer drive internally, under
/api/v1/sessions/{id}/runs,/api/v1/runs/{id}/…,/api/v1/solicitations/{id}/…,/api/v1/sessions/{id}/findings, and/api/v1/sessions/{id}/captain/…. These are machine-facing and not part of the stable public surface. - Typed per-backend passthroughs — direct, component-scoped read endpoints for
specific backends (Alertmanager, PagerDuty, Grafana, Falco, nginx, Envoy, the
datastores PostgreSQL/MySQL/Redis/MongoDB/Kafka/Elasticsearch, and the OCI/Artifactory/
ECR registries) under their own
/api/v1/<backend>/{componentID}/…paths. These are the raw layer beneath the category-based observe endpoints; prefer the observe endpoints. - LLM instrumentation and skills — model and LLM-settings management
(
/api/v1/models…,/api/v1/llm/settings…,/api/v1/llm/usage…,/api/v1/llm/providers) and skills management (/api/v1/skills…). LLM-settings writes and the per-principal usage breakdown are admin-gated; the rest are authenticated. - Warnings —
/api/v1/warnings.
There is no POST /api/v1/chat endpoint; conversational turns go through
Tasks and are recorded against Sessions. There is no HTTP endpoint
to leave safe mode — see panic and Operations.
Where to go next
- Why the surface is governed the way it is → Concepts
- Task-focused procedures that use these endpoints → Guides
- Running, observing, and recovering the daemon → Operations
- The config and environment behind the server → Configuration