Skip to content
Talk to our solutions team

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 URLhttp[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:

MethodHeader / mechanism
API keyX-Api-Key: <key>
JWTAuthorization: Bearer <jwt> (validated against the configured JWKS URL)
mTLSPresented client cert validated against the configured CA

Unauthenticated paths (always public): /health, /ready, /version.


CodeMeaning
200OK. Response body present.
201Created. Returned on initial PUT to a path that previously had no snapshot.
204No Content. Returned on successful DELETE.
304Not Modified. Returned when If-None-Match matches the current snapshot version.
400Bad Request. Malformed JSON, malformed path, etc. The body has details.
401Unauthenticated. No credentials, or credentials didn’t validate.
403Forbidden. Authenticated but the policy denies this action on this path.
404Not Found. Hive doesn’t exist, no snapshot at that path, etc.
405Method Not Allowed. e.g. PUT under /_meta/.
409Conflict. Optimistic concurrency: the If-Match header didn’t match the current version.
422Unprocessable Entity. Body failed CUE schema validation. The body has the field-level errors.
5xxServer-side failure. Retry with backoff.

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.
{
"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.


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
{ "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
{
"name": "config",
"version": "2.0.0",
"commit": "a1b2c3d",
"build_timestamp": "2026-05-20T18:00:00Z"
}

GET /hives
X-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/forge
X-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-meta
X-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-meta
X-Api-Key: $KEY
Content-Type: application/json
If-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.

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-meta
X-Api-Key: $KEY
Content-Type: application/json
If-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-meta
X-Api-Key: $KEY
If-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
}
]
}

GET /stream/config — subscribe to snapshot changes

Section titled “GET /stream/config — subscribe to snapshot changes”
GET /stream/config?hives=tenant
X-Api-Key: $KEY
Accept: text/event-stream
Last-Event-Id: 42 # optional resume cursor

Query 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: snapshot
id: 14
data: {"hive":"tenant","path":["acme","forge","prod","acme-prod","forge-meta"],"value":{...},"version":14,"provenance":{...},"schema_version":"tenant-v1","registry_version":1,"resolved_at":"..."}
event: delete
id: 15
data: {"hive":"tenant","path":["acme","forge","prod","acme-prod","forge-meta"]}
event: heartbeat
id:
data: {"ts":"2026-05-21T13:00:00Z"}
event: full_resync
id: 16
data: {"reason":"server-rebuild"}
EventWhenPayload
:connected (comment)Right after the SSE handshake completes. Bare comment line, no payload.
snapshotA new or updated snapshot for a subscribed hive.Same shape as GET /hives/<hive>/<path>.
deleteA snapshot at a path was removed.{hive, path}.
heartbeatEvery 20s. Keep-alive only — no id, no state change.{ts}
full_resyncServer 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/drift
X-Api-Key: $KEY
Content-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.


POST /register — register an instance (or upsert)

Section titled “POST /register — register an instance (or upsert)”
POST /register
X-Api-Key: $KEY
Content-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-1
X-Api-Key: $KEY

Returns 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-1
X-Api-Key: $KEY

Returns 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-west
X-Api-Key: $KEY

Query params:

  • dc — optional, filter by datacenter (read from tags.dc).
  • cluster — optional, filter by cluster (read from tags.cluster).
  • tag — optional, key:value tag 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.


GET /stream/discovery — subscribe to lease changes

Section titled “GET /stream/discovery — subscribe to lease changes”
GET /stream/discovery?services=forge-meta,forge-cli
X-Api-Key: $KEY
Accept: text/event-stream
Last-Event-Id: 102

Query params:

  • services — comma-separated list. Omit to subscribe to every service the subject is authorized to watch.

Event types:

:connected
event: instance_up
id: 103
data: {"service":"forge-meta","instance":{...same shape as instances endpoint...}}
event: instance_down
id: 104
data: {"service":"forge-meta","instance":"8f3a2b...","reason":"revoked"}
event: instance_expired
id: 105
data: {"service":"forge-meta","instance":"8f3a2b...","reason":"ttl_expired"}
event: heartbeat
id:
data: {"ts":"2026-05-21T13:00:00Z"}
EventWhen
instance_upA new lease was granted, or an existing lease’s instance details (addr, tags) changed.
instance_downRegistration was explicitly revoked via DELETE /deregister/<instance-id>.
instance_expiredTTL elapsed without a heartbeat.
heartbeatEvery 20s. Keep-alive.

Same Last-Event-Id resume semantics as /stream/config.


HeaderDirectionPurpose
X-Api-KeyRequestAPI-key authentication.
Authorization: Bearer <jwt>RequestJWT authentication.
If-MatchRequestOptimistic concurrency on writes.
If-None-MatchRequestConditional GET; 304 when unchanged.
Last-Event-IdRequestSSE resume cursor.
AcceptRequestapplication/json (default) or text/event-stream for SSE.
ETagResponseSnapshot version (also exposed as version in the body).
Content-TypeResponseapplication/json, text/event-stream, or text/plain for /_meta/schema.
X-Request-IdBothPropagated end-to-end for tracing. Server generates one if absent.

Read a tenant snapshot:

Terminal window
curl -s -H "X-Api-Key: $KEY" \
"$BASE/hives/tenant/acme/forge/prod/acme-prod/forge-meta" | jq

Write with optimistic concurrency:

Terminal window
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:

Terminal window
curl -N -H "X-Api-Key: $KEY" "$BASE/stream/config?hives=tenant"
import json, requests
from sseclient import SSEClient
KEY = "..."
BASE = "http://localhost:7123"
HDR = {"X-Api-Key": KEY}
# Read
r = 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-Match
requests.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()
# Subscribe
for 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 };
// Read
const snap = await fetch(
`${BASE}/hives/tenant/acme/forge/prod/acme-prod/forge-meta`,
{ headers: HDR }
).then(r => r.json());
// Write with If-Match
await 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);
});
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.


CodeStatusWhen
INVALID_PATH400The URL path doesn’t conform to the hive’s level shape.
BAD_JSON400Request body wasn’t valid JSON.
UNAUTHENTICATED401Missing / invalid credentials.
POLICY_DENIED403Authenticated but the hive’s access policy refuses this subject + action.
SNAPSHOT_NOT_FOUND404The path has no snapshot yet.
HIVE_NOT_FOUND404The hive doesn’t exist in the embedded registry.
INSTANCE_NOT_FOUND404The instance ID isn’t current (expired, revoked, or never registered).
IF_MATCH_MISMATCH409Optimistic concurrency: another write raced.
VALIDATION_FAILED422Body failed CUE schema. details lists field-level errors.
INTERNAL_ERROR500Server-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 with If-None-Match: "<old-version>" until you see version bump.
  • Idle SSE connections receive a heartbeat every 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-Id is propagated end-to-end. Capture it for any support-ticket you file.