Config API
Reference for the config.svc HTTP surface. For concepts (hives, sources, resolution, SSE) see the Config block docs; for config files see Configuration Files.
Canonical HTTP API for config.svc. Same surface for every caller — DevSecOps smoke-testing a fresh deploy, an application developer building against the config service, or a kis.ai platform service using the Go libraries (which call this exact API under the hood).
Base URL — http[s]://<host>:7123/. One port serves both subsystems; non-overlapping route prefixes.
Format — JSON in/out unless noted (SSE endpoints emit text/event-stream). Times are RFC 3339 UTC. Durations in JSON are nanosecond integers (Go time.Duration convention); duration fields configured in YAML accept 30s, 5m, etc.
Auth — every authenticated request needs one of:
| Method | Header / mechanism |
|---|---|
| API key | X-Api-Key: <key> |
| JWT | Authorization: Bearer <jwt> (validated against the configured JWKS URL) |
| mTLS | Presented client cert validated against the configured CA |
Unauthenticated paths (always public): /health, /ready, /version.
Conventions
Section titled “Conventions”Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
| 200 | OK. Response body present. |
| 201 | Created. Returned on initial PUT to a path that previously had no snapshot. |
| 204 | No Content. Returned on successful DELETE. |
| 304 | Not Modified. Returned when If-None-Match matches the current snapshot version. |
| 400 | Bad Request. Malformed JSON, malformed path, etc. The body has details. |
| 401 | Unauthenticated. No credentials, or credentials didn’t validate. |
| 403 | Forbidden. Authenticated but the policy denies this action on this path. |
| 404 | Not Found. Hive doesn’t exist, no snapshot at that path, etc. |
| 405 | Method Not Allowed. e.g. PUT under /_meta/. |
| 409 | Conflict. Optimistic concurrency: the If-Match header didn’t match the current version. |
| 422 | Unprocessable Entity. Body failed CUE schema validation. The body has the field-level errors. |
| 5xx | Server-side failure. Retry with backoff. |
Versioning & ETags
Section titled “Versioning & ETags”Every snapshot has a per-hive monotonic version (uint64). It doubles as the snapshot’s ETag for HTTP concurrency:
- Response headers include
ETag: "<version>"on every snapshot read. - Send
If-Match: "<version>"on PUT/PATCH/DELETE to refuse the write unless the current version matches (optimistic concurrency). - Send
If-None-Match: "<version>"on GET to short-circuit with 304 when unchanged.
Error body shape
Section titled “Error body shape”{ "code": "STRING_TAG", // e.g. "VALIDATION_FAILED", "POLICY_DENIED" "message": "human-readable summary", "details": { "field": "value", ... } // optional, structured}code is stable across releases; message is for humans; details varies per error kind.
Health & readiness (unauthenticated)
Section titled “Health & readiness (unauthenticated)”GET /health — liveness probe
Section titled “GET /health — liveness probe”GET /health{ "status": "ok" }Returns 200 if the process is alive. Suitable for k8s liveness probes / LB up-checks. Does not check downstream dependencies.
GET /ready — readiness probe
Section titled “GET /ready — readiness probe”GET /ready{ "status": "ready" }Returns 200 once the reconcilers have completed their initial pass and the binary is ready to serve traffic. Returns 503 before that (during boot, or after a fatal subsystem failure). Suitable for k8s readiness probes / LB rotation.
GET /version — build identity
Section titled “GET /version — build identity”GET /version{ "name": "config", "version": "2.0.0", "commit": "a1b2c3d", "build_timestamp": "2026-05-20T18:00:00Z"}Config subsystem
Section titled “Config subsystem”GET /hives — list declared hives
Section titled “GET /hives — list declared hives”GET /hivesX-Api-Key: $KEY{ "hives": ["cluster", "tenant"] }GET /hives/<hive>/_meta/schema — CUE schema
Section titled “GET /hives/<hive>/_meta/schema — CUE schema”Returns the CUE source for a hive’s schema as text/plain. Useful for clients that want to validate writes locally before posting.
GET /hives/<hive>/_meta/example — example value
Section titled “GET /hives/<hive>/_meta/example — example value”{ "value": { ... }}Returns an example value document that conforms to the hive’s schema — useful as a starting point when authoring a new node’s config.
GET /hives/<hive>/_meta/tree — list paths
Section titled “GET /hives/<hive>/_meta/tree — list paths”GET /hives/tenant/_meta/tree?prefix=acme/forgeX-Api-Key: $KEY{ "hive": "tenant", "paths": [ { "path": ["acme","forge","prod","acme-prod","forge-meta"], "version": 14 }, { "path": ["acme","forge","prod","other-tenant","forge-meta"], "version": 9 } ]}Query params:
prefix— optional, restrict to paths under this prefix (slash-separated).
GET /hives/<hive>/<path> — resolved snapshot
Section titled “GET /hives/<hive>/<path> — resolved snapshot”The resolved, layered, validated snapshot at a (hive, path) tuple.
GET /hives/tenant/acme/forge/prod/acme-prod/forge-metaX-Api-Key: $KEY{ "hive": "tenant", "path": ["acme","forge","prod","acme-prod","forge-meta"], "value": { ... merged config ... }, "version": 14, "provenance": { "log_level": "git-main@a1b2c3d", "features.fast_path": "pg-overrides@rowid:42" }, "schema_version": "tenant-v1", "registry_version": 1, "resolved_at": "2026-05-21T13:00:00Z"}Response headers: ETag: "14".
Path is required. Reading the root snapshot uses GET /hives/<hive>/ (trailing slash); the value is what every leaf inherits.
PUT /hives/<hive>/<path> — replace value
Section titled “PUT /hives/<hive>/<path> — replace value”Replace the entire value document at a path. Routes to whichever source in the hive’s list is writable: true with the highest priority.
PUT /hives/tenant/acme/forge/prod/acme-prod/forge-metaX-Api-Key: $KEYContent-Type: application/jsonIf-Match: "14" # optional optimistic concurrency
{ "log_level": "debug", "features": { "fast_path": true } }Response: 200 OK or 201 Created:
{ "version": 15, "resolved_at": "2026-05-21T13:01:00Z"}If-Match: "<version>" rejects with 409 Conflict if another write raced in. Re-GET, re-merge, retry.
Body validation: the merged result is run through CUE; failures return 422 with field-level details.
PATCH /hives/<hive>/<path> — deep merge
Section titled “PATCH /hives/<hive>/<path> — deep merge”Identical contract to PUT, but the body is deep-merged into the existing value rather than replacing it. Maps merge key-by-key (nested maps recurse); arrays and scalars replace.
PATCH /hives/tenant/acme/forge/prod/acme-prod/forge-metaX-Api-Key: $KEYContent-Type: application/jsonIf-Match: "14"
{ "features": { "new_dashboard": true } }DELETE /hives/<hive>/<path> — remove value
Section titled “DELETE /hives/<hive>/<path> — remove value”DELETE /hives/tenant/acme/forge/prod/acme-prod/forge-metaX-Api-Key: $KEYIf-Match: "15"Response: 204 No Content. The leaf is removed from the writable source; consumers reading this path now inherit from the parent prefix (or get {} if the root is empty).
GET /sources — declared sources per hive
Section titled “GET /sources — declared sources per hive”{ "sources": { "cluster": [ { "name": "platform-cluster-defaults", "backend": "git", "owns": ["**"], "priority": 0, "writable": false }, { "name": "cluster-overrides", "backend": "postgres", "owns": ["**"], "priority": 20, "writable": true } ], "tenant": [ ... ] }}GET /subscribers — active SSE subscribers
Section titled “GET /subscribers — active SSE subscribers”{ "subscribers": [ { "subject": "user:alice", "remote_addr": "10.0.1.5:48732", "user_agent": "lib-chassis/2.0", "hives": ["tenant"], "connected_at":"2026-05-21T13:00:00Z", "last_event": 15 } ]}Config SSE stream
Section titled “Config SSE stream”GET /stream/config — subscribe to snapshot changes
Section titled “GET /stream/config — subscribe to snapshot changes”GET /stream/config?hives=tenantX-Api-Key: $KEYAccept: text/event-streamLast-Event-Id: 42 # optional resume cursorQuery params:
hives— comma-separated list of hives to subscribe to. Omit for every hive the subject is authorized for.
Server response is text/event-stream with the following event types:
:connected
event: snapshotid: 14data: {"hive":"tenant","path":["acme","forge","prod","acme-prod","forge-meta"],"value":{...},"version":14,"provenance":{...},"schema_version":"tenant-v1","registry_version":1,"resolved_at":"..."}
event: deleteid: 15data: {"hive":"tenant","path":["acme","forge","prod","acme-prod","forge-meta"]}
event: heartbeatid:data: {"ts":"2026-05-21T13:00:00Z"}
event: full_resyncid: 16data: {"reason":"server-rebuild"}| Event | When | Payload |
|---|---|---|
:connected (comment) | Right after the SSE handshake completes. Bare comment line, no payload. | |
snapshot | A new or updated snapshot for a subscribed hive. | Same shape as GET /hives/<hive>/<path>. |
delete | A snapshot at a path was removed. | {hive, path}. |
heartbeat | Every 20s. Keep-alive only — no id, no state change. | {ts} |
full_resync | Server is asking you to drop your cache and accept the burst of snapshot events that immediately follow. | {reason} |
Resume after disconnect: cache the most recent id: you saw; on reconnect, send Last-Event-Id: <that-id>. The server backfills every snapshot with version > <that-id> then continues live streaming. Cursor 0 (or absent header) requests the current full state.
POST /stream/config/drift — subscriber drift report
Section titled “POST /stream/config/drift — subscriber drift report”POST /stream/config/driftX-Api-Key: $KEYContent-Type: application/json
{ "current_versions": { "tenant": 38, "cluster": 12 }, "hashes": { "tenant:acme/forge/prod/acme-prod/forge-meta": "sha256:..." }, "lag_seconds": { "snap-44": 1.2 }}Returns 202 Accepted. The server compares each current_versions[hive] against its own latest emitted version and emits OTel metrics on mismatches. Optional — for production observability.
Discovery subsystem
Section titled “Discovery subsystem”POST /register — register an instance (or upsert)
Section titled “POST /register — register an instance (or upsert)”POST /registerX-Api-Key: $KEYContent-Type: application/json
{ "instance_id": "swiggy-edge-1", "service": "forge-meta", "dc": "us-west", "cluster": "prod-cluster-01", "addr": "10.0.1.5:7600", "ttl_secs": 15, "tags": { "sid": "swiggy-edge-1", "version": "2.0.0", "role": "primary" }}{ "instance_id": "swiggy-edge-1", "ttl_secs": 15, "expires_at": "2026-05-21T13:00:15Z"}instance_id is optional. Pass it (typically the chassis sid) when you have a stable identity; the server uses that value as the registration handle, and a subsequent register with the same value upserts (refreshes addr, tags, TTL). Omit it and the server generates an opaque value, returned in the response. Either way, the instance_id in the response is the canonical handle for follow-up heartbeat / deregister calls.
ttl_secs must be between 5 and 600.
PUT /heartbeat/<instance-id> — extend the TTL
Section titled “PUT /heartbeat/<instance-id> — extend the TTL”PUT /heartbeat/swiggy-edge-1X-Api-Key: $KEYReturns 204 No Content. Refreshes expires_at by the originally-registered TTL. 404 if the instance ID is no longer current (expired or never registered).
DELETE /deregister/<instance-id> — remove the registration
Section titled “DELETE /deregister/<instance-id> — remove the registration”DELETE /deregister/swiggy-edge-1X-Api-Key: $KEYReturns 204 No Content. The instance disappears from query results immediately; subscribers receive an instance_down event.
GET /services — list registered services
Section titled “GET /services — list registered services”{ "services": ["config", "forge-meta", "forge-cli"] }Returns names of services with at least one live instance.
GET /services/<service>/instances — list instances
Section titled “GET /services/<service>/instances — list instances”GET /services/forge-meta/instances?dc=us-westX-Api-Key: $KEYQuery params:
dc— optional, filter by datacenter (read fromtags.dc).cluster— optional, filter by cluster (read fromtags.cluster).tag— optional,key:valuetag filter (repeatable for AND).
{ "instances": [ { "instance": "swiggy-edge-1", "service": "forge-meta", "addr": "10.0.1.5:7600", "tags": { "sid":"swiggy-edge-1", "version":"2.0.0", "dc":"us-west", "cluster":"prod-cluster-01" }, "registered_at": "2026-05-21T13:00:00Z", "ttl": 15000000000, "expires_at": "2026-05-21T13:00:15Z" } ]}The "instance" field is the stable deployment identity — the same value the caller supplied as instance_id on register (or the opaque ID the server assigned). Use it directly for heartbeat / deregister; no separate “lease ID” to track.
Discovery SSE stream
Section titled “Discovery SSE stream”GET /stream/discovery — subscribe to lease changes
Section titled “GET /stream/discovery — subscribe to lease changes”GET /stream/discovery?services=forge-meta,forge-cliX-Api-Key: $KEYAccept: text/event-streamLast-Event-Id: 102Query params:
services— comma-separated list. Omit to subscribe to every service the subject is authorized to watch.
Event types:
:connected
event: instance_upid: 103data: {"service":"forge-meta","instance":{...same shape as instances endpoint...}}
event: instance_downid: 104data: {"service":"forge-meta","instance":"8f3a2b...","reason":"revoked"}
event: instance_expiredid: 105data: {"service":"forge-meta","instance":"8f3a2b...","reason":"ttl_expired"}
event: heartbeatid:data: {"ts":"2026-05-21T13:00:00Z"}| Event | When |
|---|---|
instance_up | A new lease was granted, or an existing lease’s instance details (addr, tags) changed. |
instance_down | Registration was explicitly revoked via DELETE /deregister/<instance-id>. |
instance_expired | TTL elapsed without a heartbeat. |
heartbeat | Every 20s. Keep-alive. |
Same Last-Event-Id resume semantics as /stream/config.
Headers reference
Section titled “Headers reference”| Header | Direction | Purpose |
|---|---|---|
X-Api-Key | Request | API-key authentication. |
Authorization: Bearer <jwt> | Request | JWT authentication. |
If-Match | Request | Optimistic concurrency on writes. |
If-None-Match | Request | Conditional GET; 304 when unchanged. |
Last-Event-Id | Request | SSE resume cursor. |
Accept | Request | application/json (default) or text/event-stream for SSE. |
ETag | Response | Snapshot version (also exposed as version in the body). |
Content-Type | Response | application/json, text/event-stream, or text/plain for /_meta/schema. |
X-Request-Id | Both | Propagated end-to-end for tracing. Server generates one if absent. |
Code samples (audience-neutral)
Section titled “Code samples (audience-neutral)”Read a tenant snapshot:
curl -s -H "X-Api-Key: $KEY" \ "$BASE/hives/tenant/acme/forge/prod/acme-prod/forge-meta" | jqWrite with optimistic concurrency:
VER=$(curl -s -H "X-Api-Key: $KEY" \ "$BASE/hives/tenant/acme/forge/prod/acme-prod/forge-meta" | jq -r .version)
curl -si -X PUT \ -H "X-Api-Key: $KEY" \ -H "Content-Type: application/json" \ -H "If-Match: \"$VER\"" \ -d '{"log_level":"debug"}' \ "$BASE/hives/tenant/acme/forge/prod/acme-prod/forge-meta"Subscribe to SSE:
curl -N -H "X-Api-Key: $KEY" "$BASE/stream/config?hives=tenant"Python (requests + sseclient-py)
Section titled “Python (requests + sseclient-py)”import json, requestsfrom sseclient import SSEClient
KEY = "..."BASE = "http://localhost:7123"HDR = {"X-Api-Key": KEY}
# Readr = requests.get(f"{BASE}/hives/tenant/acme/forge/prod/acme-prod/forge-meta", headers=HDR)r.raise_for_status()snap = r.json()print("version =", snap["version"])
# Write with If-Matchrequests.put( f"{BASE}/hives/tenant/acme/forge/prod/acme-prod/forge-meta", headers={**HDR, "If-Match": f'"{snap["version"]}"', "Content-Type": "application/json"}, json={"log_level": "debug"},).raise_for_status()
# Subscribefor ev in SSEClient(f"{BASE}/stream/config?hives=tenant", headers=HDR): if ev.event == "snapshot": snap = json.loads(ev.data) print(snap["path"], snap["version"])JavaScript / TypeScript (fetch + EventSource)
Section titled “JavaScript / TypeScript (fetch + EventSource)”const KEY = '...';const BASE = 'http://localhost:7123';const HDR = { 'X-Api-Key': KEY };
// Readconst snap = await fetch( `${BASE}/hives/tenant/acme/forge/prod/acme-prod/forge-meta`, { headers: HDR }).then(r => r.json());
// Write with If-Matchawait fetch(`${BASE}/hives/tenant/acme/forge/prod/acme-prod/forge-meta`, { method: 'PUT', headers: { ...HDR, 'If-Match': `"${snap.version}"`, 'Content-Type': 'application/json' }, body: JSON.stringify({ log_level: 'debug' }),});
// Subscribe — note: native EventSource doesn't support custom headers;// for the API key, either use a polyfill (eventsource-polyfill) or// authenticate via a cookie/query param (server-supported in some// deployments via a per-environment shim).const es = new EventSource(`${BASE}/stream/config?hives=tenant`);es.addEventListener('snapshot', e => { const snap = JSON.parse(e.data); console.log(snap.path, snap.version);});Go (stdlib net/http, no external libs)
Section titled “Go (stdlib net/http, no external libs)”package main
import ( "encoding/json" "fmt" "io" "net/http")
const ( base = "http://localhost:7123" key = "...")
type Snapshot struct { Hive string `json:"hive"` Path []string `json:"path"` Value map[string]any `json:"value"` Version uint64 `json:"version"`}
func main() { // Read req, _ := http.NewRequest("GET", base+"/hives/tenant/acme/forge/prod/acme-prod/forge-meta", nil) req.Header.Set("X-Api-Key", key) resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() var snap Snapshot _ = json.NewDecoder(resp.Body).Decode(&snap) fmt.Printf("v=%d val=%v\n", snap.Version, snap.Value)
// Subscribe (read SSE line-by-line) sseReq, _ := http.NewRequest("GET", base+"/stream/config?hives=tenant", nil) sseReq.Header.Set("X-Api-Key", key) sseReq.Header.Set("Accept", "text/event-stream") sse, _ := http.DefaultClient.Do(sseReq) defer sse.Body.Close() buf := make([]byte, 4096) for { n, err := sse.Body.Read(buf) if err == io.EOF || n == 0 { break } fmt.Print(string(buf[:n])) }}For kis.ai platform developers, don’t use the stdlib path — the chassis Go libraries handle SSE resume, bbolt cache, change callbacks, and resilience-by-default for you. See Platform Integration via the Chassis.
Errors you’ll actually hit
Section titled “Errors you’ll actually hit”| Code | Status | When |
|---|---|---|
INVALID_PATH | 400 | The URL path doesn’t conform to the hive’s level shape. |
BAD_JSON | 400 | Request body wasn’t valid JSON. |
UNAUTHENTICATED | 401 | Missing / invalid credentials. |
POLICY_DENIED | 403 | Authenticated but the hive’s access policy refuses this subject + action. |
SNAPSHOT_NOT_FOUND | 404 | The path has no snapshot yet. |
HIVE_NOT_FOUND | 404 | The hive doesn’t exist in the embedded registry. |
INSTANCE_NOT_FOUND | 404 | The instance ID isn’t current (expired, revoked, or never registered). |
IF_MATCH_MISMATCH | 409 | Optimistic concurrency: another write raced. |
VALIDATION_FAILED | 422 | Body failed CUE schema. details lists field-level errors. |
INTERNAL_ERROR | 500 | Server-side failure. Retry with backoff; check server logs. |
- All snapshot reads and SSE streams are eventually consistent against the source of truth — for the strongest guarantee, write with
If-Match, then re-read withIf-None-Match: "<old-version>"until you see version bump. - Idle SSE connections receive a
heartbeatevery 20s. If you go 60s without any event (heartbeat or otherwise), reconnect. - Server enforces a per-subject SSE concurrency limit (default 16). Excess connections receive 429.
X-Request-Idis propagated end-to-end. Capture it for any support-ticket you file.