| title | HTTP API schema |
|---|---|
| description | Generate clients and inspect Kata's versioned OpenAPI schema, compatibility rules, and authentication. |
| last_edited | 2026-10-10 |
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 to discover the endpoint and
transport for those requests with the same selection rules as the CLI. Go
programs can use the generated Go client, which includes
discovery. Native cron documents job and workflow resources.
The schema is committed at api/openapi.yaml and regenerated by the kata
binary:
kata openapi > openapi.yamlkata 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.
Daemon API errors use an error object with a stable code and a readable
message. Optional hint text suggests a next step; optional data contains
structured details. When a kata command can produce the error, its hint names
CLI commands and flags, such as --force, rather than request fields. Hints for
errors that only direct API calls can produce may name request fields. Use the
code to branch in scripts, rather than matching the message or hint.
{"error":{"code":"project_not_initialized","message":"no .kata.toml ancestor and no git ancestor","hint":"run \"kata init --project <name>\" in the workspace root, or pass --project <name>"}}The CLI preserves the hint in human, agent, and JSON errors. See agent output for its stderr format.
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.
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": 29,
"api_schema_version": "0.23.0",
"version": "1.4.2",
"uptime": "5m0s",
"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. Status-filtered search
requires API 0.20.0; label-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. Status-filtered kata.search calls additionally check for
API 0.20.0 before project fanout. Paginated kata.list and explicit
single-project lexical kata.search require API 0.27.0. The CLI checks the
same version before new list options and explicit lexical pagination.
Close requests may explicitly supply transcript (HTTP API 0.26.0):
{
"actor": "agent-one",
"reason": "done",
"message": "Implemented the requested behavior and ran the focused tests.",
"evidence": [{"type": "test", "command": "go test ./internal/example"}],
"transcript": {
"agent": "codex",
"session_id": "00000000-0000-4000-8000-000000000001",
"url": "https://agentsview.example/sessions/codex/00000000-0000-4000-8000-000000000001"
}
}The agent is codex or claude, session_id is its native UUID, and url is
optional absolute HTTP(S) without credentials, query, or fragment. Supply a
verified locator for namespaced imported archives. The daemon stores this
separately from evidence in the close event; it does not fetch or verify the
chat. Its agent and session_id participate in retry identity; url does
not. The object is preserved in scoped responses and history. Automatic capture is a CLI client preference,
not daemon-side session discovery.
Guarded close requests use a request-local compatibility check instead of a
separate health probe. A request that sends Idempotency-Key or If-Match
also sends retry_protocol: "close-v1". Current daemons require the marker;
older daemons reject the unknown field before the close can run.
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.
API contract versions advance independently of Kata release versions.
The current main-branch contract is API 0.27.0. Kata 0.18.0 includes the changes from
0.18.0 through 0.21.0 below. Teammate comments require API 0.18.0, issue-scoped
credentials use 0.19.0, status-filtered search requires 0.20.0, and
oldest-first lists require 0.21.0.
Kata 0.17.0 included API changes 0.15.0 through 0.17.0. Custom clients
upgrading from earlier releases must send array fields as [] (or omit
optional fields), use the exact request field names, and accept empty response
collections as [] or {}.
| Version | Change |
|---|---|
0.27.0 |
Adds filter-bound creation cursors, complete, optional filtered total, priority=none, and repeatable project_ids to issue listing. Explicit lexical search with sort=oldest supports continuation beyond the ranked candidate cap. |
0.26.0 |
Close requests accept optional transcript provenance, stored alongside evidence in close events and retained in scoped history and retry receipts. |
0.23.0 |
Added transactional move previews with the optional dry_run request field. Preview responses omit new_short_id until a move allocates the target ID. Generated clients now type new_short_id as optional; read issue.short_id for the issue's current short ID. |
0.22.0 |
Added temporary assignments: claim requests accept ttl_seconds, claim responses return ordered events, issue projections include assignment_expires_on, and assignment renewal and expiry have distinct event types. |
0.21.0 |
Added optional sort=oldest to both issue-list routes. Matching rows are ordered by created_at ascending and id ascending before limit; omission preserves each route's default. |
0.20.0 |
Added optional status=open or status=closed to project search. Omission searches both statuses; explicit empty values are invalid. The predicate applies to lexical candidates and canonical semantic hits before result limits. |
0.19.0 |
Added issue-subtree token scope and expiration, capability discovery, and redacted credential lifecycle state with a server observation time. Health storage, embedding, and federation diagnostics are optional and depend on caller authority. |
0.18.0 |
Comments accept optional teammate attribution and preserve it in responses and retry fingerprints. |
0.17.0 |
Create issue, edit issue, create comment, and add label accept a numeric project ID or name:<project> in the project path. Optional alias headers use alias-first resolution. Successful responses include X-Kata-Project-Name. Generated clients represent these four path parameters as strings. |
0.16.0 |
Ordinary API array fields are non-null. Empty and nil Go slices serialize as [], generated clients omit null from ordinary array types, and requests reject null for those arrays. JSON object member names are case-sensitive. |
0.15.0 |
Added close idempotency and revision headers, the close-v1 request marker, and retry receipt fields. |
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. |
The 0.16.0 wire format uses Go's JSON v2 defaults. Besides non-null slices,
this means nil maps encode as {}, duplicate object member names and invalid
UTF-8 are rejected, and struct field names match case-sensitively. JSON object
member order is unspecified. Unknown request members remain governed by the
request schema and are rejected for strict request objects.
GET /api/v1/health without credentials provides liveness and version fields.
db_path, embeddings, and federation_config are optional diagnostics:
authenticated unscoped principals and the owner-local transport receive them
when configured. Public remote probes and scoped credentials do not. An
explicit scoped bearer remains scoped over loopback; an invalid bearer returns
an authentication error. Clients must handle the public response without these
fields. idle_shutdown keeps its availability rules described above.
POST /api/v1/tokens accepts scope and expires_in_seconds together:
{
"actor": "worker-a",
"name": "worktree-a",
"scope": {
"kind": "issue_subtree",
"project_uid": "01J00000000000000000000002",
"root_issue_uid": "01J00000000000000000000001"
},
"expires_in_seconds": 14400
}The scope kind is fixed, both UIDs identify an active issue in its active
project, and expiration is a positive whole number of seconds within the
server's duration range. The server calculates the deadline. Identity mode and
token-administration authority are required; a spoke project is refused with
scoped_token_spoke_forbidden. Scope, actor, and expiration are immutable.
There is no renewal or worker-issued delegation endpoint.
Authenticated GET /api/v1/instance advertises issue_subtree_tokens. Clients
check it before minting; a missing or false field means the request is
unsupported. Token creation returns one-time plaintext and redacted token
metadata. CLI and MCP provisioners write scoped plaintext to a protected file
and withhold it from output; see token CLI usage.
The instance auth object defines caller authority. Browser session responses,
UI snapshot capabilities, and UI reference capabilities carry the same fields:
| Field | Meaning |
|---|---|
scope |
kind, project_uid, and root_issue_uid; absent for an unscoped principal. |
expires_at |
The scoped token's absolute RFC 3339 UTC deadline. |
allowed_actions |
A fixed set derived from daemon policy; callers cannot select individual permissions. |
close_requires_evidence |
Requires the normal close message and typed evidence contract, including for TUI requests. |
token_audit_read |
Allows the redacted token inventory, independently of ordinary issue write access. |
Writable scoped credentials receive issue.read, issue.create_child,
issue.edit, issue.comment, issue.labels, issue.metadata, issue.assign,
issue.claim, issue.close, issue.reopen, issue.link, issue.lease, and
activity.read. Read-only policy retains only issue.read and activity.read.
Browser writable, updates, and actor_policy retain their existing meaning.
Clients reject unknown or incomplete scoped authority. Scope support alone
grants no token-administration permission, and domain constraints can still
refuse an otherwise allowed action.
GET /api/v1/tokens returns observed_at and redacted token rows, including
scope, expires_at, and state. States use that one observation time:
revoked takes precedence over expired, and all remaining rows are live.
A live row does not prove the worker is running or its root remains usable.
See credential auditing for
operator access and last-use limitations.
Scoped project responses retain required field types: metadata is an empty
object and created_at is a zero timestamp. An alias-resolution response retains
an empty alias object. These placeholders do not expose private project data.
The scope guide owns membership,
operation constraints, and event invalidation and revocation.
POST /api/v1/projects/{project_id}/issues/{ref}/actions/claim accepts an
optional ttl_seconds integer from 60 through 86400. Omitting it creates a
permanent assignment when the issue is unassigned. Repeating a timed claim by
the current actor renews its expiry; repeating a permanent claim preserves any
existing expiry. if_unowned refuses every live assignment, including one
held by the same actor, while force can replace another actor.
The response's events array is ordered. A takeover of an expired assignment
returns issue.assignment_expired before the new issue.assigned event. The
singular event remains the final event for compatibility. Renewal emits
issue.assignment_renewed. Issue projections expose the nullable absolute UTC
deadline as assignment_expires_on.
At the expiry boundary, ready and next treat the issue as unassigned even
if the cleanup worker has not recorded the expiry event yet. Cleanup later
clears the stored owner and deadline conditionally, so a concurrent renewal or
takeover is preserved.
For a federated spoke, the hub is authoritative for temporary claims. The
spoke forwards the request with its enrollment credential, persists the
returned events in order, and does not fall back to a local claim when the hub
is unavailable. A hub may include replay_events with the assignment history
needed to apply a renewal after the spoke missed earlier events. The spoke
persists those prerequisites before the mutation events and omits them from
the client-facing response.
These operations accept either a numeric project ID or a name: selector:
| Method | Path |
|---|---|
POST |
/api/v1/projects/name:example-project/issues |
PATCH |
/api/v1/projects/name:example-project/issues/{ref} |
POST |
/api/v1/projects/name:example-project/issues/{ref}/comments |
POST |
/api/v1/projects/name:example-project/issues/{ref}/labels |
URL-encode the project selector as one path segment. Numeric paths such as
/api/v1/projects/42/issues remain valid. The issue ref and mutation body
keep their existing meanings. A successful response includes the canonical
project name in X-Kata-Project-Name.
A name: request can also carry X-Kata-Project-Alias (the workspace alias
identity) and X-Kata-Project-Alias-Kind (git or local). The alias takes
precedence over the name, preserving rename and merge resolution. If the
alias is unknown, the existing named project is used and the alias is
attached. name: with no name requires a registered alias. Resolution does
not create projects, and numeric selectors do not accept alias headers.
Alias attachment requires attributed write authority and is unavailable to local browser sessions. When a host access controller is installed, named resolution requires all-project authority before resolution or attachment, matching the standalone project-resolve endpoint. Project-scoped clients can continue using numeric IDs.
Both GET /api/v1/projects/{project_id}/issues and GET /api/v1/issues
accept sort=oldest (creation ascending) and sort=created (creation descending).
Both break equal created_at timestamps by id in the same direction and
apply the order before limit. sort=oldest requires API 0.21.0; the following
pagination contract and sort=created require API 0.27.0.
Responses include issues, complete, truncated, and next_cursor when
another page exists. Pass the opaque next_cursor as cursor with the same
explicit sort and filters. limit=0 returns all remaining matches. Optional
include_total=true adds total, a SQL count with the same filters and
permissions, independent of the cursor and page size.
Use priority=none for issues with unset priority. It is mutually exclusive
with max_priority; numeric priority accepts 0 through 4. max_priority
excludes unset priorities. The global route also accepts repeated positive
project_ids values for one filtered page across selected projects. An empty
or invalid selection is rejected. project_ids and project_id are mutually
exclusive, and every selected project requires authorization.
Omitting sort, or sending an empty value, keeps the project route's
updated_at descending default and the global route's created_at descending
default. The project default cannot continue with a cursor; use an explicit
creation sort for exhaustive scans.
Ordinary edits preserve creation order. Cursors validate the daemon instance, project selection, authorization scope, sort, and filters; changing these returns HTTP 400. The page size and count request may change between pages. These are live reads rather than snapshots. Restart after changes to filter membership, project membership, deletion, or imported creation timestamps.
GET /api/v1/projects/{project_id}/search accepts cursor only with explicit
mode=lexical&sort=oldest. It returns oldest-creation pages using the same
next_cursor, complete, and truncated fields, without the ranked search's
200-candidate ceiling. Query, filters, daemon, and authorization scope must
stay the same. Search does not provide total.
Other searches retain ranked ordering and bounded candidate retrieval.
They have no continuation cursor. complete is false for truncated or
degraded results and whenever ranked retrieval cannot establish exhaustion;
a full candidate window must not be treated as the whole matching set.
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_versionchange. Response objects are published withadditionalProperties: 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: falseby default, so unknown request fields are rejected unless a type explicitly opts in. New optional request fields and query parameters do bumpapi_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, leavesadditionalPropertiesunset on response schemas instead: the same permissive meaning, phrased so generators model optional object-valued response fields (such asparent) 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 toapi_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.yamldrifts 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.
GET /api/v1/doctor supplies the daemon-side hook findings used by
kata doctor. Its hooks block includes current
executable/working-directory availability, queue capacity and occupancy,
process-lifetime dropped events, and counts of runs and failures from the last
seven days found in scanned history. history_truncated marks a scan limited
by the eight-MiB or 64-rotated-file budget; counts may omit recent runs.
history_incomplete marks missing, unreadable, or malformed history. These
flags are independent: a capped scan can also encounter a read error.
Arguments, environment, hook output and raw errors are
never returned. available: false means the hook sink does not supply
diagnostics.
The endpoint requires owner-local or configured static/bootstrap-token
authority. Mounted hosts authorize it as a non-mutating
integration_administration operation with the manage capability. Ordinary
identity tokens, browser sessions, trusted-proxy principals and unauthenticated
private-network modes cannot access these process diagnostics. A 404 can mean
the endpoint is unsupported or hidden from this principal.
The optional /health embeddings block also includes error_present when the
reconciler has an error, including failures without a provider HTTP status.
This boolean does not disclose the provider's raw diagnostic text.
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/snapshotreturns one coherent catalog, collection, optional selected detail/history/graph projection, capability envelope, durable event cursor, and strong ETag. -
GET /api/v1/ui/referencesreturns bounded projects, owners, labels, and qualified issue references for pickers and typeahead. Repeatingissue_uidhydrates 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:{"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=1fragment 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; send the issued cookie, X-Kata-Web-Session,
X-Kata-CSRF, and the exact configured Origin. A successful logout returns
204 and revokes that tab's session immediately, including its event stream;
other tabs remain valid. /api/v1/ui/session/logout is not a route, and there
is no browser-session refresh endpoint.
CLI federation enrollment can use the hub's admin bearer token, even when the
web UI uses login mode. The CLI reads the variable named by
--hub-token-env <env-name> or uses a daemon catalog entry whose URL matches
the hub origin; it never forwards the
local daemon's global token to the hub. Ordinary API requests without browser
credentials use daemon bearer authentication on both browser and shared TCP
listeners. Host validation still applies; requests carrying browser credentials
or Fetch Metadata follow browser session and Origin checks.
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.
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.
Enrollment creation accepts both lease and claim; the API returns sorted,
de-duplicated capabilities using the canonical claim name. A project-scoped
grant with a supplied hub URL requires enabled hub federation. Once federation
is enabled, creation returns a join object containing
its hub project ID/UID, local project name, baseline and replay cursors, actor,
capabilities, token, transport options, and join_command. Supply hub_url
as an HTTP(S) base URL reachable by the spoke, optionally with a federation
API reverse-proxy prefix. The server does not infer a federation address
from its browser origin; dedicated browser listeners and authentication proxies
may not serve federation transport. Non-loopback HTTP URLs, including private
IP addresses, require allow_insecure: true. Without it, enrollment returns
400 before issuing a token. The returned join command carries
--allow-insecure; HTTPS still verifies certificates.
The returned command includes --project and the hub project ID, plus any
push/adoption options. Join fetches the current UID and cursors from the hub
when it runs, so a purge after enrollment does not leave it using an old baseline.
Treat the response and command as secrets. Wildcard grants have no project join object; grants without pull, or servers without a
supplied hub URL, have no runnable join command. Legacy API callers
omitting hub_url may issue a grant before enabling federation; those responses
omit join until hub metadata exists. Listing grants
never returns plaintext tokens or join commands.
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 and the caller may read
health diagnostics, 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.
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.
Send Idempotency-Key when creating a comment you may need to retry. For seven
days, repeating the same issue, actor, body, and key returns the original
comment. Keys belong to individual issues, so a retry can survive an issue
move between projects. Reusing the key on that issue with a different actor or
body returns a conflict. Use the full issue UID to identify the issue across
moves.
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.
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.
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 are project-scoped and provider-qualified. They configure
one-way external issue sync into the Kata project. Providers github and
notion back the kata sync github ... and kata sync notion ... commands.
A project can have only one external issue-sync binding.
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 initially; omission on re-enable preserves the saved choice. Set it to
false to retain GitHub issue titles and add the plain github label. Explicit
true restores the [GitHub #123] prefix. Local title edits and local labels
are preserved, and matching upstream github labels are deduplicated. The daemon validates the repository with its configured
GitHub credential chain, stores the repository identity, and returns the
binding and status.
For Notion, use /api/v1/projects/{project_id}/issue-sync/notion/enable.
All Notion reads use the daemon's token environment variable, selected by
[notion_sync].token_env (default KATA_NOTION_TOKEN). The enable request accepts:
Notion config key |
Meaning |
|---|---|
data_source_id |
Data source UUID. Initial enable requires this or database, never both. |
database |
Database UUID or supported Notion URL containing exactly one data source. |
status_property |
Status property ID or exact name; omitted initially selects the sole Status property. |
assignee_property |
People property ID or exact name. A People property is required; omit the selector initially only when exactly one exists. |
done_statuses |
Array of completed status IDs or exact names; at least one is required initially. |
title_prefix |
Boolean, initially true. False keeps source titles and adds the notion label. |
since |
Exclusive updated-after UTC date or whole-second RFC3339 timestamp; empty clears the cutoff. |
Notion re-enable preserves omitted selections, interval, cutoff, and title-prefix
choice. The response config contains resolved data_source_id, database_id,
title_property_id, status_property_id, assignee_property_id,
done_status_ids, since, and title_prefix. Source and mapping IDs cannot
change on re-enable. See Notion sync for access,
field ownership, polling limits, and recovery.
A concurrent binding edit during enable returns HTTP 409 with error code
issue_sync_binding_changed. Read current status before retrying the intended
change. A Notion upstream deadline during enable returns HTTP 504 with error
code notion_timeout.
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 source identity. Replace
github with notion in the disable, status, and once paths for Notion bindings.
Read status:
GET /api/v1/projects/{project_id}/issue-sync/github/statusStatus 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 while the claim remains active. Operator edits fence an older run from further writes/imports while retaining
its claim until it finishes or the stale horizon expires.
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, status_sync mode, pending_count, and the last attempt, success, error, and import counts. |
status_updated |
Present on once; incoming status transitions committed separately from content imports. |
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: newer GitHub updates can overwrite local edits to those fields.
Explicit close/reopen writes back when status_sync is two-way; other fields
remain incoming-only.
V1 does not support outbound fields other than status, timeline events, pull requests, deleted or transferred issue propagation, edited or deleted comment propagation, or multiple assignees beyond the first GitHub assignee.
Plane uses the same issue-sync routes with provider plane. Initial enable config
requires string workspace and UUID string project_id; optional since and
boolean title_prefix preserve omission on re-enable. Top-level status_sync
selects one-way or two-way; omitted mode preserves the saved choice. Optional
config strings closed_state_id and open_state_id select UUIDs in the completed
and unstarted groups. Empty strings clear overrides. Intervals accept one of
interval or interval_seconds. Origins and tokens are daemon-owned and rejected
as request config. Source identity is immutable; validation reads project and
states before a guarded upsert. See Plane sync.
Linear uses the issue-sync routes with provider linear. Initial enable config
requires string workspace_id and team_id UUIDs. Optional string project_id
restricts the team to a project. Scope is immutable. Optional since,
closed_state_id, and open_state_id are strings; title_prefix is a boolean.
Top-level status_sync and interval use the existing issue-sync contract.
Re-enable preserves omitted options. Credentials and endpoint selection are
not accepted in binding requests. See Linear sync.
Twenty uses the same issue-sync routes with provider twenty. Enable discovers
and verifies workspace identity from the daemon-owned credential. Optional config
keys are string since, boolean title_prefix, string closed_status, string
open_status, and string array open_statuses. Top-level status_sync selects
one-way or two-way; interval accepts interval or interval_seconds.
Omitted settings preserve saved values. Origins, credential selectors, keys,
and workspace IDs are rejected as request config. Status classification is
validated against live metadata before a guarded upsert. Empty or
whitespace-only closed_status and open_status values are rejected; omit a
key to preserve its saved value. See Twenty sync.
Todoist uses the issue-sync routes with provider todoist. Initial enable config
requires opaque string project_id. Optional string history_since selects a
UTC date or whole-second RFC3339 completion floor; omission initially chooses
thirty days ago. Boolean title_prefix, top-level status_sync, and interval
preserve omission on re-enable. The daemon resolves account_id. Account,
origin, project and history floor are immutable. Requests cannot supply tokens
or origins. Two-way delivery uses completion/reopen endpoints after fresh
safety checks. See Todoist sync.
TickTick uses the same issue-sync routes with provider ticktick. Initial
enable config requires string project_id; optional boolean title_prefix
preserves omission on re-enable. The project ID is immutable. Top-level
status_sync selects one-way or two-way; interval accepts interval or
interval_seconds. Tokens and the API origin are daemon-owned and rejected as
request config. Enable reads the project to validate its kind, archive state,
and permission before a guarded upsert. See
TickTick sync.