---
last_edited: 2026-08-27
---

# HTTP API schema

The daemon exposes an HTTP API (used by the CLI, TUI, and remote clients). Its
shape is published as an OpenAPI 3.1 document so out-of-process clients can
generate typed clients instead of hand-copying wire structs.

Use [`kata daemon locate`](daemon-discovery.md) to discover the endpoint and
transport for those requests with the same selection rules as the CLI.

## Getting the schema

The schema is committed at `api/openapi.yaml` and regenerated by the `kata`
binary:

```sh
kata openapi > openapi.yaml
```

`kata openapi` builds the document in-process from the daemon's route
definitions, so it needs neither a running daemon nor a database. The runtime
`/openapi.json` route stays disabled; the committed artifact and this command
are the supported way to obtain the schema.

## What the schema is

A **per-release snapshot** of the daemon's current HTTP API: a faithful
description of the routes and wire types as they exist at that commit. It is
useful for client generation and review.

It is **not** a promise of a forever-stable API. Treat it as the contract for
the daemon version you generated it against, not a guarantee that a future
daemon will accept the same calls.

## Detecting the API version

The schema carries a version in its `info.version` field
(`APISchemaVersion`). The same value is reported at runtime by
[`GET /api/v1/health`](#) as `api_schema_version`:

```json
{
  "ok": true,
  "schema_version": 7,
  "api_schema_version": "0.14.0",
  "version": "1.4.2",
  "uptime": "5m0s",
  "db_path": "/path/to/kata.db",
  "idle_shutdown": {
    "timeout": "15m0s",
    "state": "armed",
    "deadline": "2026-08-17T17:15:00Z"
  }
}
```

`idle_shutdown` is present only when this process is an implicitly started,
owner-local daemon with an effective timeout. Its state is one of:

| State | Meaning |
| --- | --- |
| `armed` | No foreground request is active; `deadline` is the next eligible shutdown time. |
| `foreground` | Client work is active and the idle deadline is suspended. |
| `blocked` | The prior deadline elapsed, but already-admitted finite background work is still draining. |
| `stopping` | New work is no longer admitted and root shutdown has started. |

`deadline` is present for `armed` and `blocked`, and absent while foreground
work suspends the deadline or shutdown is already committed. Monitoring reads
of health, ping, and instance state do not renew the deadline.

Three distinct version fields appear here; they answer different questions:

| Field | Meaning |
| --- | --- |
| `api_schema_version` | The HTTP API contract version; match this against the schema you generated your client from. |
| `schema_version` | The database/storage schema version (`meta.schema_version`), an internal storage concern. |
| `version` | The daemon build version. |

A client can read `api_schema_version` before it uses a versioned request
feature. Versions increase in semantic-version order, so a client can compare
the daemon version with the first version that contains the feature. This
minimum-version check proves that the request field exists; it does not prove
that every part of a generated client matches a different schema version.
Generated clients should still use the schema from their target release.

The first-party CLI uses this check before requests where an older daemon
could ignore a filter and return unfiltered rows. Filtered `search` and
filtered `ready --all` require API `0.8.0`; filtered `list --all` requires API
`0.9.0`. The CLI stops before the filtered query and tells the operator to
upgrade when the daemon is too old. `kata mcp serve` performs the same check
once at startup and requires API `0.11.0`, because its relationship tools
always send the pinned-target fields and its close-audit paging relies on
`event_id`.

The field is **optional in the schema** even though current daemons always send
it. That is deliberate: a version-detection field has to survive version skew,
so a client generated from a schema that includes it can still parse the
response of an older daemon that predates it. Treat an **absent or empty**
`api_schema_version` as "a daemon older than this field," not a parse error.
Embedding hosts using `@kenn-io/kata-ui` must treat that state as incompatible
and decline to render issue detail.

### Version history

| Version | Change |
| --- | --- |
| `0.14.0` | Added `external` close evidence with its required `account` field. |
| `0.13.0` | Added optimistic revision guards to issue and project metadata patch requests. |
| `0.12.0` | Added the optional `idle_shutdown` health block for effective auto-start idle shutdown state and capability discovery. |
| `0.11.0` | Added optional `to_project_uid` on create-time initial links and `expected_project_uids` on edit link deltas so clients can pin resolved relationship targets to immutable project identities inside the mutation transaction. Close-audit rows gain a required `event_id` (stable pagination cursor) and optional `parent_uid`. |
| `0.10.0` | Added the repeatable `issue_uid` query parameter to UI references so embedding hosts can hydrate summaries by stable issue UID. |
| `0.9.0` | Added owner, label, exclusion, and metadata filters to the cross-project issue list. Global list rows now include `project_name`. |
| `0.8.0` | Added repeatable `label` and `exclude_label` query parameters to project-scoped search. It also identifies daemons that support filtered global ready queries. |
| `0.7.0` | Added transactional federation enrollment rotation, idempotent replay semantics for caller-supplied enrollment tokens, and the optional `federation_config` health block used by startup reconciliation. |
| `0.6.0` | Ready responses now return fully hydrated issues. Project-scoped ready rows are `IssueOut` instead of the slimmer `Issue` projection, and global ready rows (`ReadyGlobalIssueOut`, renamed from `ReadyGlobalIssue`) embed `IssueOut` plus `project_name`, so both gain `labels`, `qualified_id` (required), link peers (`parent`, `blocks`, `blocked_by`, `related`), `blocked`, and `child_counts`. Clients regenerated from the schema see the new component name; the shipped Go client keeps `ReadyGlobalIssue` as a deprecated alias. |
| `0.5.0` | Added metadata patch endpoints for issues and projects, issue create metadata, and metadata-key filters on project issue lists. Generated clients can patch metadata with `If-Match` revisions, send initial metadata in create requests, and request selected metadata keys with the `meta` query parameter. |
| `0.4.0` | Added semantic-search response mode metadata and embeddings health fields. Search responses now always include the effective `mode`; configured daemons may also report sanitized embedding reconciler status from `/health`, including backlog and provider HTTP status without raw provider diagnostics. |
| `0.3.0` | Moved the author identity rewrite endpoint from `POST /api/v1/projects/{project_id}/federation/rewrite-author` to `POST /api/v1/projects/{project_id}/actions/rewrite-author`. The operation is project current-state hygiene rather than a federation command, so generated clients should use the project action route. |
| `0.2.0` | Removed `links[].project_id` from link projections. Links are now project-independent edges that may span projects, so a single `project_id` no longer describes a link. `links[].from` and `links[].to` (and the edit response's `changes` block peers) gain `project` and `qualified_id`, always populated. `IssueOut.parent_short_id` is replaced by `parent` (a `LinkPeer` object with all four fields). The cross-project link feature lands across this version. Event payloads are unchanged: a cross-project link mutation currently emits its event in the subject issue's project only (mirrored peer-project events are planned). |
| `0.1.0` | Initial published contract. |

## Compatibility expectations

These are the current intentions, not a contractual guarantee:

- **Additive changes are not signalled.** New endpoints, and new optional
  response fields, can appear without an `api_schema_version` change. Response
  objects are published with `additionalProperties: true`, so a client generated
  from the schema tolerates fields it does not yet know about even under strict
  validation. The response relaxation never loosens request schemas: those keep
  the strictness their types declare: `additionalProperties: false` by default,
  so unknown request fields are rejected unless a type explicitly opts in.
  New optional request fields and query parameters do bump
  `api_schema_version`: generated clients may emit them, and older strict
  daemons may reject them.
  The OpenAPI 3.0 flavor (`kata openapi --version 3.0`), which exists as
  code-generator input, leaves `additionalProperties` unset on response
  schemas instead: the same permissive meaning, phrased so generators model
  optional object-valued response fields (such as `parent`) as pointers
  rather than always-present values.
- **Breaking changes bump `api_schema_version`.** Removing or renaming a field,
  changing a field's type, or removing an endpoint is a breaking change and is
  signalled by a change to `api_schema_version`. A client that pins or checks
  the value it was generated against can detect the mismatch instead of failing
  at an arbitrary call site.
- **Regeneration stays honest.** A committed golden test fails if
  `api/openapi.yaml` drifts from the routes, so the published schema cannot
  silently fall out of date with the daemon it describes.

If you build against this schema and hit a gap, please open an issue. The
contract is meant to be useful to external clients, and feedback shapes how far
the compatibility guarantees are taken.

## Browser UI endpoints

The daemon serves the production browser application and its canonical routes
from the same origin as the API. Browser reads use two native projection
endpoints published in OpenAPI:

- `GET /api/v1/ui/snapshot` returns one coherent catalog, collection, optional
  selected detail/history/graph projection, capability envelope, durable event
  cursor, and strong ETag.
- `GET /api/v1/ui/references` returns bounded projects, owners, labels, and
  qualified issue references for pickers and typeahead. Repeating `issue_uid`
  hydrates summaries for up to 200 stable issue UIDs and composes with the
  existing project and text filters.
- `GET /api/v1/ui/launch-target?issue_uid=<uid>` validates one active issue and
  returns a safe absolute route into the standalone browser application:

  ```json
  {"available": true, "url": "https://kata.example/kata?issue=<uid>#direct=1"}
  ```

  When no credential-safe browser origin is configured, it returns HTTP 200
  with `{"available":false,"reason":"browser_origin_unavailable"}` and no
  URL. Unknown or deleted issues, archived projects, and host-denied resources
  remain not found. The `#direct=1` fragment keeps the standalone application
  on the daemon that issued the target even when its roster defaults to a
  different remote daemon.

A matching snapshot `If-None-Match` returns `304` without rebuilding the
projection. The response cursor is captured in the same consistent storage
read as a newly built projection. Browser events are invalidations; the SPA
refreshes the snapshot instead of constructing a second authority from event
payloads.

The browser-session routes are deliberately outside the generated client
contract. A fresh tab on a direct-loopback origin uses
`POST /api/v1/ui/session/local`. Exact Host, peer address, forwarding-header,
and identity/proxy authentication policy gate that endpoint before it issues a
local-web session. A static daemon token does not disable this owner-local path.
The mint normally requires the exact Origin; an embedded owner-local browser may
omit or send an empty Origin only on this endpoint, while cross-site Fetch Metadata
remains forbidden. Exact Host validation runs first and remains the DNS-rebinding
boundary.

The URL reported by `kata daemon status` and local `kata ui` both open that
loopback origin directly. Configured origins exchange a bearer or identity
token at `POST /api/v1/ui/session/login`. Logout is
`DELETE /api/v1/ui/session`; there is no browser-session refresh endpoint.
Session-authentication errors advertise the canonical browser origin in
`X-Kata-Web-Origin` and its `loopback` or `login` mode in
`X-Kata-Web-Authentication`, allowing `kata ui` to distinguish a configured
direct loopback URL from a login-only deployment without carrying a
launch credential.

The launch-target URL is derived only from the daemon's configured canonical
browser origin. Request `Host`, `Origin`, and forwarding headers never supply
authority, and origins with credentials, path prefixes, query strings, or
fragments are unavailable rather than rewritten.

After session creation, every browser data request requires both the instance
cookie and `X-Kata-Web-Session`. Browser data mutations additionally require
the exact `Origin` and `X-Kata-CSRF`; the protected event stream carries the
session header and `Last-Event-ID` through `fetch`. In unauthenticated
`--insecure-readonly` mode, the UI advertises read-only authority and polls
snapshots because the event stream remains protected.

## Federation Enrollment and Health Endpoints

`POST /api/v1/federation/enrollments` accepts an optional `token`. Omitting it
preserves create-once behavior and returns a newly generated plaintext
enrollment token. When the caller supplies a token, an exact retry with the
same spoke instance UID, project ID, canonical capabilities, resolved actor,
and adoption policy returns the same active enrollment. Reusing that token with
different attributes, or after its enrollment was revoked, returns `409` with
`federation_enrollment_token_conflict`. Token-auth identity resolution happens
before this comparison, so a DB-backed identity token's actor overrides the
request body's `actor`.

`POST /api/v1/federation/enrollments/actions/rotate` repairs a project-scoped
spoke whose local binding exists but whose enrollment credential was lost. It
uses the same normal hub administration authorization as enrollment creation
and requires `spoke_instance_uid`, a positive `project_id`, `capabilities`, and
an explicit replacement `token`; `actor` and
`allow_adoption_snapshot_authors` have the same meaning as on creation. In one
transaction the hub revokes active grants for that spoke and project and
installs the replacement. After canonical capability normalization and
token-authenticated actor resolution, retrying the same replacement token with
the same spoke instance UID, project ID, canonical capabilities, resolved
actor, and adoption policy returns the same active enrollment. An attribute
mismatch or a revoked replacement enrollment returns `409` with
`federation_enrollment_token_conflict`.

`POST /api/v1/federation/replicas/{project_id}/actions/leave` accepts two
mutually exclusive coordination flags. `preflight=true` performs the existing
read-only eligibility check. `prepare=true` validates the same local
conditions, durably marks any config-managed credential as leaving, blocks new
config reconciliation for that mapping, and waits for earlier enrollment or
rotation requests to drain. Its `pending_enrollment` response gives the
non-secret hub coordinates needed for revoke. The client then performs hub
cleanup and calls the endpoint again without either flag for authoritative
local teardown. Repeating prepare and finalization is safe after interruption.

`POST /api/v1/federation/replicas/{project_id}/actions/rebind` accepts only
`{"hub_catalog":"<name>"}`. The server resolves that name from its own daemon
catalog; callers cannot supply a replacement URL or credential. It requires a
remote HTTPS entry, deliberately sends the spoke's existing enrollment token
to that configured endpoint, and proceeds only when federation metadata
returns the persisted hub project ID and UID. Catalog administration tokens
are never resolved for this call. Success returns the local project,
display-safe old and new origins, and `rebound`, `resumed`, or `unchanged`.
Retries safely converge from fully old, credential-moved-first, binding-moved-
first, or fully migrated state.
Before local convergence, the action drains project-scoped federation
transport using the old endpoint and prevents new transport from starting
until both stores name the target.

When config-driven mappings are present, `GET /api/v1/health` adds an optional
`federation_config` object:

| Field | Meaning |
| --- | --- |
| `configured` | Number of startup mappings. |
| `reconciled` | Mappings that reached the requested active binding. |
| `pending` | Mappings awaiting their first attempt or retrying a runtime failure. |
| `conflicted` | Mappings whose latest attempt found incompatible configuration, credentials, or a local binding. They continue retrying in case an operator resolves the conflict. |
| `last_attempt_at` | Latest attempt time across all mappings, when any attempt has run. |
| `last_success_at` | Latest successful reconciliation time across all mappings, when any has succeeded. |
| `last_error_category` | Sanitized category from the most recent failed attempt; omitted until a failure occurs. |
| `last_error_status` | Remote HTTP status for that failure when one is available; omitted otherwise. |

Error categories are `configuration_conflict`, `binding_conflict`,
`credential_io`, `hub_unavailable`, `hub_authentication`, `hub_validation`,
`local_storage`, and `internal`. `internal` indicates an unexpected sanitized
reconciler failure.

The block is absent when no mappings are configured. It never exposes tokens,
hashes, headers, URLs, project names, actors, or raw remote response bodies.
Reconciliation is fail-open: `ok` remains `true` while mappings are pending or
conflicted, so hub availability does not control spoke readiness.

## Comment Endpoints

Comments can be appended with `POST
/api/v1/projects/{project_id}/issues/{ref}/comments` and edited in place with
`PATCH /api/v1/projects/{project_id}/issues/{ref}/comments/{comment_ref}`.
Use the comment UID as `comment_ref`; numeric comment IDs are local storage
artifacts.

Editing a comment overwrites the current comment body while preserving the
original author, UID, creation time, and thread position. This is the supported
primitive for redacting comment text before enrolling a project in federation.
It is not a purge mechanism for data that has already crossed into another
federated event log.

## Project Hygiene Endpoints

`POST /api/v1/projects/{project_id}/actions/rewrite-author` rewrites one exact
author identity in the project's current rows. The request body carries
`actor`, `from`, and `to`; `to` must be non-empty. The daemon updates issue
authors, issue owners, comment authors, and link authors that exactly match
`from`, emits one `project.author_rewritten` event when anything changed, and
returns per-field counts. The endpoint refuses already-federated projects
because it is a current-state hygiene operation for local project rows, not a
federated history rewrite.

## Issue Graph Endpoint

`GET /api/v1/projects/{project_id}/issues/{ref}/graph` returns the relationship
graph reachable from one source issue. The path `ref` accepts the same issue ref
forms as other project-scoped issue endpoints. The optional `depth` query value
is either `full` (the default) or a non-negative hop count such as `1`, `2`, or
`3`. Pass `hide_done=true` to keep the source issue but exclude closed
non-source issues from traversal and output.

The response body is a canonical graph payload:

| Field | Meaning |
| --- | --- |
| `source_uid` | Stable UID of the source issue. |
| `depth` | Effective traversal depth, either `full` or the bounded hop count. |
| `hide_done` | Whether closed non-source issues were hidden. |
| `nodes` | Reached issues, including normal issue fields plus `qualified_id` (`project#short_id`) for display. |
| `edges` | Directed relationship edges with `from_uid`, `to_uid`, `kind`, and `layout`. |
| `unresolved_refs` | Link endpoints that exist in storage but could not be materialized as graph nodes. |

Edge direction is normalized for clients: parent edges point parent -> child,
`blocks` edges point blocker -> blocked, and related edges use kata's canonical
related-link ordering. `layout=false` keeps an edge in the graph while telling
layout engines they can omit it from force/layout calculations; current daemons
use that hint for transitive `blocks` edges.

Soft-deleted issues and issues in archived projects are hidden instead of
reported as unresolved references. `unresolved_refs` is reserved for dangling
endpoints that can appear after imports, federation repair, or manual database
maintenance, so graph clients can surface incomplete data without discarding
the reachable graph.

## Issue Sync Endpoints

Issue sync endpoints are project-scoped and provider-qualified. They configure
one-way external issue sync into the kata project. GitHub is the only provider
implemented in v1, and these endpoints back the `kata sync github ...`
commands.

All GitHub access happens in the daemon process. For provider `github`, v1
accepts `github.com` and exact GitHub Enterprise hostnames listed in
`KATA_GITHUB_SYNC_ALLOWED_HOSTS`. The daemon resolves credentials in this
order: a matching `[[github_sync.app]]` entry, the explicit token env named by
`[github_sync].token_env` (default `KATA_GITHUB_TOKEN`) only when
`[github_sync].token_host` matches the binding host, then `gh auth token
--hostname <host>` as a local fallback. Remote clients call these endpoints on
the daemon, so client-side GitHub credentials are not sufficient when the
daemon runs elsewhere, for example behind `https://daemon.example`.

The GitHub App credential path is the recommended shared-daemon deployment
model. The App only needs Metadata read and Issues read permissions. The `gh
auth token` fallback is best suited to local and single-user daemons.
GitHub-sourced parent links are imported when the host exposes the queried
parent fields; unsupported schemas are nonfatal and preserve existing
source-managed parent links.

The API surface is provider-neutral so future providers such as GitLab or
Linear can use the same lifecycle endpoints. Provider-specific identity lives
inside the `config` object, which must not contain raw credentials; providers
that need credentials should use a credential reference or a daemon-local auth
mechanism.

JSONL restore imports issue sync bindings as disabled. Re-enable them locally
after verifying the restored host and daemon credentials.

Enable sync:

```http
POST /api/v1/projects/{project_id}/issue-sync/github/enable
Content-Type: application/json

{
  "config": {
    "host": "github.com",
    "owner": "example-org",
    "repo": "example-repo",
    "title_prefix": true
  },
  "interval": "5m"
}
```

For GitHub, `config.host` defaults to `github.com`. Clients may send
`interval_seconds` instead of `interval`. `config.title_prefix` defaults to
`true`; set it to `false` to preserve GitHub issue titles without the
`[GitHub #123]` prefix. The daemon validates the repository with its configured
GitHub credential chain, stores the repository identity, and returns the
binding and status.

Disable sync:

```http
POST /api/v1/projects/{project_id}/issue-sync/github/disable
Content-Type: application/json

{}
```

Disabling stops polling but keeps the binding, cursor, status, and import
mappings so re-enabling can resume against the same repository identity.

Read status:

```http
GET /api/v1/projects/{project_id}/issue-sync/github/status
```

Status returns a `not_enabled` state when the project has no binding. Existing
disabled bindings are returned with their last run status.

Run once:

```http
POST /api/v1/projects/{project_id}/issue-sync/github/once
Content-Type: application/json

{}
```

`once` runs one immediate daemon-side sync for an enabled binding. It bypasses
the interval schedule but still respects the in-flight guard; overlapping runs
return a conflict.

The shared response body contains:

| Field | Meaning |
| --- | --- |
| `binding` | The stored provider binding, including provider, stable source key, remote ID, display name, opaque config, enabled flag, interval, cursor, and timestamps. It is absent only for a project with no binding for the requested provider. |
| `status` | Current state and the last attempt, success, error, and import counts. |
| `import` | Present only on `once`; reports created, updated, unchanged, comment, and link counts from the import run. |

Synced issues are GitHub-owned for title, body, state, labels, owner, and
imported GitHub comments. API clients should treat those fields as read-mostly
in kata: kata does not write back to GitHub, and newer GitHub updates can
overwrite local issue or comment edits to those fields.

V1 does not support GitHub write-back, timeline events, pull requests, deleted
or transferred issue propagation, edited or deleted comment propagation, or
multiple assignees beyond the first GitHub assignee.
