skip to content

// Automation

automation api

the loopback JSON api — every endpoint, auth, and streaming events.

automation api

kern exposes a plain-http JSON api bound to `127.0.0.1` only (never the lan). kern-cli is a client of this api; anything else — scripts, editors, ci — can be too.

enable it under settings → automation & cli. the endpoint and bearer token are published to automation.json in the app data directory:

json
{ "version": 2, "port": 7442, "token": "…64 hex chars…", "pid": 12345, "started_at": 1789237726 }
platformapp data directory
windows%APPDATA%\com.ellio.kern
macos~/Library/Application Support/com.ellio.kern
linux$XDG_DATA_HOME/com.ellio.kern (or ~/.local/share/com.ellio.kern)

every request needs Authorization: Bearer <token>:

bash
TOKEN=$(jq -r .token "$APPDATA/com.ellio.kern/automation.json") curl -s -H "Authorization: Bearer $TOKEN" http://127.0.0.1:7442/status
powershell
$ep = Get-Content "$env:APPDATA\com.ellio.kern\automation.json" | ConvertFrom-Json Invoke-RestMethod -Uri "http://127.0.0.1:$($ep.port)/status" -Headers @{ Authorization = "Bearer $($ep.token)" }

// note

GET /status reports apiVersion. this docs page describes api v2 (kern v0.3.0+). v1 clients (/status, /servers, /servers/{id}/log, lifecycle, stdin) keep working — v2 only adds endpoints and fields.

conventions

  • all requests and responses are JSON (content-type: application/json).
  • errors are { "error": "message" } with a 4xx/5xx status.
  • start/install answer 200 once the action is underway. stop/restart/backup/restore answer 202 accepted and finish in the background — poll the resource to observe completion.
  • path segments are percent-encoded (backups/world%202026.zip/restore).
  • request bodies are capped at 64 KiB; log reads at 2 MiB.

endpoints

app

methodpathreturns
GET/status (alias /health){ status, version, apiVersion, pid, host: { cpu, ram } }
GET/host/metrics{ cpu, ram, status: "host" }
GET/audit?limit=100&since=<epoch>{ entries: [...], now }
GET/events?since=<epoch>&wait=30{ entries, statuses: { id: status }, now }

servers

methodpathnotes
GET/serverslist; add ?ports=1 for a live port scan per running instance
POST/serverscreate; body { name, serverType, path, group?, tags?, autoStart?, imported?, userOverrides? }201
GET/servers/{id}detail: config + pid, uptimeSecs, metrics, ports, lastCrash
PATCH/servers/{id}sparse update: name, group (null clears), tags, autoStart, stopCommand, stopTimeoutSecs, userOverrides
DELETE/servers/{id}?folder=1remove the record; folder=1 also deletes the working directory
POST/servers/{id}/start200 { action: "started" }
POST/servers/{id}/stop202 — graceful stdin → timeout → force-kill
POST/servers/{id}/restart202
POST/servers/{id}/install200 — runs the plugin's install step
POST/servers/{id}/stdinbody { "line": "say hi" } (raw text also accepted)
GET/servers/{id}/log?lines=200&offset=<bytes>{ lines, nextOffset, size, reset, running }
GET/servers/{id}/metrics?window=3600{ windowSecs, samples: [{ at, cpu, ram }] }
GET/servers/{id}/energy{ id, hours, estWatts, cost, currencyNote }
GET/servers/{id}/preflight{ conflicts: [{ port, pid, process }], eulaPending, lowDisk, freeMb }
GET/servers/{id}/crash`{ crash: null \{ at, exitCode, forced, tail } }`
GET/servers/{id}/tasks{ tasks: [...] }
POST/servers/{id}/tasks/{taskId}/run{ ok: true }
GET/servers/{id}/backups{ backups: [{ name, size, created }] }
POST/servers/{id}/backup202 — snapshot now
POST/servers/{id}/backups/{name}/restore202 — world is snapshotted before the overwrite
DELETE/servers/{id}/backups/{name}{ ok: true }
GET/servers/{id}/files?path=<rel>{ entries: [{ name, isDir, size, modified }] }path defaults to the instance root
GET/servers/{id}/file?path=<rel>{ content, mtime } — mtime feeds the write conflict check
PUT/servers/{id}/filebody { path, content, expectedMtime? }{ mtime }; when expectedMtime doesn't match on-disk, returns an error starting with conflict:
POST/servers/{id}/filesbody `{ op: "mkdir" \"delete" \"delete_recursive" \"rename", path, to? }`
GET`/servers/{id}/search?q=&mode=contents\filenames\both&include=&exclude=`{ matches: [{ relPath, lineNumber?, linePreview? }] }
GET/servers/{id}/snapshots?path=<rel>{ snapshots: [{ id, at, size }] } — per-file editor history
GET/servers/{id}/snapshot?path=&id={ content }
POST/servers/{id}/snapshotsbody { path }{ id } (null when nothing changed)
POST/servers/{id}/snapshots/restorebody { path, id }
DELETE/servers/{id}/snapshotsbody { path, id }
PUT/servers/{id}/tasksbody { tasks: [...] } — replaces the instance's schedule
GET/servers/{id}/backup-schedule{ intervalSecs, keep, onStop, lastBackupSecs }
PUT/servers/{id}/backup-schedulebody = the same shape
GET/servers/{id}/snippets["say hi", ...]
PUT/servers/{id}/snippetsbody { snippets: [...] }
GET/servers/{id}/rcon{ host, port, hasPassword }
GET/servers/{id}/players{ players, raw } — executes RCON list
GET/servers/{id}/log/downloadraw latest.log with an attachment filename
POST/plugins/upload-install?name=x.kernraw .kern body — validates + installs, returns the manifest summary
GET/registry/plugins?q=&category=&sort=marketplace listing through the host's registry client
POST/registry/installbody { slug, version }202 { jobId }
GET/jobs/{id}`{ id, kind, state: running\done\error, message, at }`
GET/audit/downloadraw audit log with an attachment filename
GET/inspect?path=<dir>import inspection: jars, start scripts, world/eula flags, suggested runtime/name

plugins

methodpathnotes
GET/pluginsinstalled manifests
POST/plugins/installbody { path, force? } — path to a local .kern201 manifest
POST/plugins/validatebody { path } → `{ valid, manifest \error }` (never errors on a bad package)
DELETE/plugins/{id}uninstall

streaming logs without re-reading

/servers/{id}/log is offset-based. start with offset=0 to get the tail, then keep the returned nextOffset:

bash
OFFSET=0 while true; do BODY=$(curl -s -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:7442/servers/srv_123/log?lines=200&offset=$OFFSET") echo "$BODY" | jq -r '.lines[]' OFFSET=$(echo "$BODY" | jq -r .nextOffset) sleep 1 done

reset: true means the log rotated or shrank; the response contains a fresh tail — clear your buffer and continue from nextOffset. a trailing partial line is held back until it completes, so no output is ever split mid-line.

long-poll events

/events merges audit entries with the current status map. with wait=30 it blocks until something new arrives or the wait elapses, which makes it a cheap push feed:

bash
SINCE=$(date +%s) while true; do BODY=$(curl -s -H "Authorization: Bearer $TOKEN" \ "http://127.0.0.1:7442/events?since=$SINCE&wait=30") echo "$BODY" | jq -r '.entries[] | "\(.at) \(.action) \(.detail)"' echo "$BODY" | jq -r '.statuses | to_entries[] | "\(.key): \(.value)"' SINCE=$(echo "$BODY" | jq -r .now) done

entries are oldest-first ({ at, action, detail, serverId? }). statuses is the full id → status map; diff consecutive responses to catch crash/restart transitions that don't produce an audit entry.

wait is capped at 30 seconds; keep your http timeout above it.

creating an instance end-to-end

bash
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"name":"Prod API","serverType":"custom","path":"/srv/api","group":"prod","tags":["live"]}' \ http://127.0.0.1:7442/servers # → 201 { "id": "srv_a1b2c3", ... } curl -s -X POST -H "Authorization: Bearer $TOKEN" \ http://127.0.0.1:7442/servers/srv_a1b2c3/start

status codes

codewhen
200success
201created (server, plugin install)
202accepted — stop/restart/backup/restore run in the background
400invalid body, missing parameter, or a failed validation
401missing/incorrect bearer token
404unknown server/plugin/backup, or unknown route
413 / 431body / headers too large
500internal error

// warn

the api cannot create or delete users (there are none) and never binds beyond 127.0.0.1. for phone control over the lan see web remote — a separate, token-paired https server.

clients

kern-cli wraps every endpoint with typed output. for anything it doesn't cover, kern-cli api is a raw passthrough:

bash
kern-cli api GET /servers kern-cli api PATCH /servers/srv_a1b2c3 --body '{"group":"staging"}'
raw markdown ↗
updated 2026-09-12edit on github ↗