HTTP API schema

August 5, 2026 · View on GitHub

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.

Getting the schema

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

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:

{
  "ok": true,
  "schema_version": 7,
  "api_schema_version": "0.7.0",
  "version": "1.4.2",
  "uptime": "5m0s",
  "db_path": "/path/to/kata.db"
}

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

FieldMeaning
api_schema_versionThe HTTP API contract version — match this against the schema you generated your client from.
schema_versionThe database/storage schema version (meta.schema_version), an internal storage concern.
versionThe daemon build version.

A client can read api_schema_version once at startup and decide whether it recognizes the daemon's API before issuing further calls. Compare it for equality against the info.version of the schema the client was generated from: the value is semver-shaped, but no ordering semantics are defined, so treat any other value as "verify compatibility" rather than inferring safety from an unchanged major or minor component.

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.

Version history

VersionChange
0.7.0Added 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.0Ready 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.0Added 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.0Added 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.0Moved 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.0Removed 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.0Initial 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.

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 keyless direct-loopback origin uses POST /api/v1/ui/session/local; Host, Origin, peer address, forwarding-header, and daemon-auth policy gate that endpoint before it issues a local-web session. 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 keyless loopback URL from an authenticated deployment without carrying a launch credential.

Every browser data request requires both the instance cookie and X-Kata-Web-Session. 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:

FieldMeaning
configuredNumber of startup mappings.
reconciledMappings that reached the requested active binding.
pendingMappings awaiting their first attempt or retrying a runtime failure.
conflictedMappings whose latest attempt found incompatible configuration, credentials, or a local binding. They continue retrying in case an operator resolves the conflict.
last_attempt_atLatest attempt time across all mappings, when any attempt has run.
last_success_atLatest successful reconciliation time across all mappings, when any has succeeded.
last_error_categorySanitized category from the most recent failed attempt; omitted until a failure occurs.
last_error_statusRemote 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:

FieldMeaning
source_uidStable UID of the source issue.
depthEffective traversal depth, either full or the bounded hop count.
hide_doneWhether closed non-source issues were hidden.
nodesReached issues, including normal issue fields plus qualified_id (project#short_id) for display.
edgesDirected relationship edges with from_uid, to_uid, kind, and layout.
unresolved_refsLink 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:

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:

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:

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:

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:

FieldMeaning
bindingThe 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.
statusCurrent state and the last attempt, success, error, and import counts.
importPresent 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.