ConduitDocs

API

The panel exposes a JSON HTTP API under /api. Every route documented here exists in dashboard/src/app/api/** — methods and auth were read from the handlers, not inferred.

Authentication#

Three ways in, checked by lib/auth.ts / middleware.ts:

MethodHowNotes
Session cookieconduit_s — set by POST /api/auth/login, passkey login, or SSOHMAC-signed, 7-day TTL, per-device session id
Personal API tokenAuthorization: Bearer cu_<secret>Created on the account page; sha256-stored, shown once. Supports readOnly, expiry, and area scopes; scoped tokens can never touch /api/account*
Machine tokenAuthorization: Bearer <token> equal to CONDUIT_AGENT_TOKENFull access at the middleware, for ops scripts and the Discord bot. The connector's own token opens connector paths only. Machine paths require a token by default; CONDUIT_ENFORCE_MACHINE_AUTH=0 opts out
Fleet grantAuthorization: Bearer <grant secret> from a joined clusterOnly the /api/fleet/* paths listed under Joined clusters; each grant carries the paths it may open

Roles: admin, operator, developer (scoped to assigned groups), viewer (read-only). Sensitive vaults (mail, domains, system credentials) additionally require sudo mode — a 5-minute elevation from POST /api/auth/reauth.

Who a route answers to#

The middleware lets the machine token through everywhere, but a handler decides who it is talking to in one of two ways, and the tables below say which:

  • requester() (lib/requester.ts): a session, a personal token, or the machine token, which counts as an admin named "machine". A request the relay executes for a joined cluster carries the machine token plus a signed hop naming a person, and is answered as that person.
  • currentUser() only: a session or a personal token. The machine token is refused with 401 on these routes, marked session only below. Routes that act on "your own account" need an account id, so they refuse the machine token too.
  • Grant-gated: public at the middleware (it runs on the edge, with no store to read a grant from), verified in the handler against the grant's own paths, constant-time.

Two query parameters address another cluster (lib/cluster-params.ts). ?cluster=<id> on an instance action or anything under /api/services/<vmid>/ acts on that cluster's nodes directly over the mesh, with the credentials it issued, so it works while its panel is down; it is refused for a scoped account, for any role but admin or operator, and while the cluster has issued no reach. ?estate=<id> on an edit of a store record forwards the request to the panel that owns the record through its relay, as the person asking; only a signed-in, unscoped account may cross. Both take the cluster's own id. Sending either empty is a 400, never "local".

The same request, three ways — a session login followed by a read of the fleet overview:

# Session login: the cookie jar holds conduit_s afterwards.
curl -s -X POST http://panel:3001/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"name":"admin","password":"…"}' -c cookies.txt

# Or skip the session entirely and use a personal token.
curl -s http://panel:3001/api/overview -H 'Authorization: Bearer cu_…'

Conventions#

  • Requests and responses are JSON (uploads use multipart).
  • Errors: { "error": "message" } with a 4xx/5xx status.
  • Mutations (POST/PATCH/PUT/DELETE) are recorded in the operator audit log with actor, method, and path.
  • Failed logins answer after a uniform 400ms delay — no timing or existence oracle.

5xx does not carry a message anybody will read#

A panel behind Cloudflare cannot use a 5xx status to deliver an explanation. Cloudflare treats an origin 5xx as its own concern: it discards the body and serves a branded error page instead. Measured on this cluster — a 90-byte JSON explanation replaced by 6419 bytes of 502: Bad gateway HTML — and the browser then reports SyntaxError: unexpected character at line 1 column 1, which names the JSON parser and nothing that actually happened.

So the rule for handlers:

  • 4xx for a request that could not be served as asked (unknown id, wrong software, not running, forbidden). These reach the client untouched, body included.
  • 200 with ok: false and an error string for an operation that ran and did not succeed. The request was served; the outcome is the failure, and an outcome belongs in the body.
  • 5xx only for genuinely unhandled faults, where nobody is relying on the text.

Swept across every handler on 2026-08-25: 26 sites moved off 5xx, and lib/api-fail.test.ts now fails the build if a new one appears. failed(e) in lib/api-fail.ts is the one implementation — 409 rather than 400 because these are almost never malformed requests: the request was fine and the cluster could not honour it, and 400 would blame the caller for the cluster's state.

/api/metrics/prometheus is the deliberate exception, for the opposite reason to everything else: nobody reads its text, a scraper reads the status. A 200 carrying an error comment would be recorded as a successful scrape of zero metrics — indistinguishable from a healthy idle cluster — so the 500 is the signal, and it is scraped inside the network rather than through the CDN.

services/[vmid]/hytale-auth is the worked example. On the client, lib/read-json.ts (readJson / fetchJson) reads a reply without letting a non-JSON one masquerade as a parser bug, and names the intermediary when a reply was replaced in transit. Prefer it to fetch(...).then(r => r.json()).

Route families#

Method lists are exact; per-field shapes live in the handlers.

118 of 118 endpoints

Monitoring

Servers

Settings

Network

Players

Platform

Nodes

Files

Users

Backups

Databases

Storage

Auth & account#

RouteMethodsAccessPurpose
/api/auth/login · logout · me · setupPOST/GETpublic / sessionPassword + TOTP login, session teardown, identity, first-admin bootstrap
/api/auth/passkeys (+ /register, /login)GET/POST/PUT/DELETEsession · login publicWebAuthn: list/remove, registration ceremony, usernameless sign-in
/api/auth/reauthPOSTsessionSudo-mode elevation (5 min)
/api/auth/reset-request · reset-completePOSTpublicMail-token password reset
/api/auth/sso/*GETpublicOAuth (Google / Discord / GitHub) start, callback, provider list
/api/accountGET/PATCHsession onlyYour profile, tokens, live sessions and sign-in history, with cluster (which one keeps the account) and foreign. PATCH sets the avatar, alert opt-ins, or starts a verified recovery-email change (password re-checked); refused with 409 for an account another cluster keeps
/api/account/password · tokens · ssh-keysPOST/PATCH/DELETE (GET for keys)a signed-in account; the machine token is nobody and is refusedChange your password; mint (shown once), edit the lock of, or revoke a personal token (readOnly, expiresInDays, scopes); paste or generate an ssh key for the CLI gateway (private key returned once). Each runs on the cluster that keeps the account: ?estate=<id> relays it there as you, and a request for a replicated account without it answers 409 naming that cluster. The result replicates within a tick, so a token works on every joined panel
/api/account/sessions · logout-all · totp · verify-email · console-sessionsPOST/GET/DELETEsession only; never relayedRevoke one device, sign out everywhere (bumps the session epoch, which replicates), 2FA, confirm a recovery email, ssh console sessions. A session is the panel's that minted it; a foreign account's sessions on this panel live beside it, not on its home cluster
/api/users (+ [id], [id]/tokens)GET/POST/PATCH/DELETEadmin (session or machine token); users.view / users.manageGET lists the whole fleet's accounts, each with cluster and sameNameOn (two accounts with one name on two clusters are two accounts). A name is unique across the fleet. Create, edit (role, custom roleId, extras, scope, disabled, password) and delete run on the cluster that keeps the account: ?estate=<id> relays them there as the admin asking, otherwise a foreign account answers 409. Last-admin, no-self-delete and self-lockout guards
/api/rolesGET/POST/PATCH/DELETEusers.view / users.roles; requesterEvery role the fleet has, built-ins first, each with its expanded capabilities, holders across every cluster and the cluster that keeps it. A grant beyond what the editor holds is refused (cannot grant what you do not hold: ...); a built-in cannot be edited or deleted; a role in use anywhere cannot be deleted; ?estate= relays an edit to the cluster that keeps the role. The icon is local and settable on any role

Fleet & infrastructure#

RouteMethodsAccessPurpose
/api/groups (+ [id], subgroups, broadcast)GET/POST/PATCH/DELETErole-gatedGroups, subgroups, slot limits, maintenance
/api/tasks (+ [id], update, resync, motd, sharding, env)POST/PATCH/DELETErole + group scopeServices: scale, software, MOTD, world sharding, shared time/weather
/api/tasks/[id]/shards · timemachine · replay · portalsGET/POST/DELETErole + group scopePer-strip claims and the live split (Elastic World), Time Machine captures/rewind, Ghost Replay scrubber data, cross-server portal regions
/api/tasks/[id]/canary · prewarm · builds · reachabilityGET/POSTsessionCanary rollout state, predictive pre-warm, source-build pipeline, public-path reachability check
/api/containers (+ [vmid], action, migrate)GET/POST/DELETEsessionRaw LXC view, power actions, node migration
/api/instances/bulkPOSTsessionBulk instance operations
/api/nodes · /api/overview · /api/conduit/state · /api/conduit/reconcileGET/POSTsessionCluster snapshot, full state read, manual reconcile kick
/api/blueprints (+ [id], bundle, import-bundle) · /api/templates · /api/global-templatesGET/POST/PATCH/DELETEsessionBlueprints and file-overlay templates
/api/firewallGET/POSTsession · admin mutationsToggle, per-service ingress, manual rules, access lists, node protection
/api/domainsGET/POSTsession only; network.domains; POST admin + sudoGET: the zone and whether a token and an Origin CA key are stored, shareWithFleet, every hostname row with verified, the certificate, the per-CT rollout, the primary, and the public address with its source (override / env / detected / unknown). POST takes one action per body: {zone, cfToken?, cfOriginKey?} saves credentials and resolves the zone id; {entry:{hostname, target, customTarget?, proxied, failover?}} upserts a row and its A record (an ssh row is always DNS-only; an ssh, packs or cluster row replaces the existing one of that kind, DNS record included; failover is accepted on panel rows and needs a connected zone); {removeEntry: id}; {issueCert: true}; `{setPrimary: hostname
/api/domains/check · /api/domains/challengePOST · GETcheck: admin session; challenge: publiccheck {hostname} mints a one-time challenge and returns DNS diagnostics plus the URL the browser should fetch; challenge?token= is fetched through the hostname itself and stamps the row verified only when the token and the Host header both match
/api/backups (+ jobs, restore, coverage, capture, staging)GET/POST/PATCH/DELETEsession · restore adminProxmox vzdump storages, schedules, restore; coverage is the per-world data-safety report (vzdump + strip snapshots + Time Machine + db dumps + shared-volume capture, 60s cache); POST capture takes a shared-volume capture now; POST staging reclaims shard-transfer leftovers
/api/drillsGET/POSTsession · run adminRestore drills — prove a backup restores; verdicts per world, scratch containers destroyed after
/api/offsiteGET/POSTadminEncrypted off-site copies of the cluster store to any S3-compatible bucket. GET never returns the secret key or the encryption key — only a fingerprint of the latter. POST {action:"save"|"run"|"verify"}: save writes config (a blank secret leaves the stored one alone), run sends one copy now, verify downloads the newest and decrypts it
/api/logs/searchGETsessionFleet-wide log search. ?q= (≥2 chars) &since= &regex=1 &vmid=. Fans out to every running container's agent in parallel; each greps its own log files and journal. Returns hits grouped by service plus unsearched[] — containers with no agent, or that failed, named with the reason
/api/jobsGETsessionHealth of the leader's 30 recurring background jobs — last outcome, duration, consecutive failures, plus pending for known jobs with no run since this boot (the union of live and persisted state, so the list never shrinks after a restart). Served from the leader's memory, or the shared store on a passive replica. 24 run on the ten-second reconcile chain; the six that do real I/O when they fire have their own minute interval
/api/diagnostics/memoryGETadminWhat this replica is holding: V8 heap used against its own reported ceiling, RSS, and every in-process registry by size. Samples a few entries per collection rather than serializing them, so it stays safe to call on a process that is already near its limit. Per-replica — call it against a replica's own address. heap.prior carries what the previous process peaked at and which jobs were in flight then, read back from disk after a restart, so the reading is not lost with the process that took it
/api/network/managedGET/POSTsession · adminConduit's own container subnet: current config, each node's /24, addresses assigned, and any overlap with a route a node already has. POST {enabled, base?, bridge?} — refused with 409 if the range collides
/api/fleet and /api/fleet/*The joined clusters: see Joined clusters below
/api/wireguardGET/POSTsession · admin to POSTThe node mesh: one row per node with its mesh address, a truncated public key, how long ago another node last heard from it, and rx/tx throughput plus cumulative bytes read from wg show dump. totals sums the fleet and counts how many nodes are actually linked. Rates are null rather than 0 when they cannot yet be known — a first sample, or a counter reset by an interface restart. POST {enabled} turns the mesh on or off
/api/dev-workspaces (+ /:id)GET/POST/PATCH/DELETEsession (developer = own only)Temporary personal server copies: GET lists them with live instances and the scope-filtered source picker; POST clones a source task; PATCH {ttlHours} extends the TTL measured from now (capped 168h); DELETE tears one down early
/api/databasesGET/POST/PATCH/DELETEsession only; databases.view / databases.manage; writes adminManaged databases with their connection strings, masked (***) unless the caller's sudo re-auth is fresh (revealed), plus live connection usage per system engine. POST {name, engine} provisions a database and a login scoped to it; PATCH {id, exposePublic} opens or closes the engine port on its node through the firewall; DELETE ?id= drops it. See databases.md
/api/databases/{id}GET/POST/DELETEsession only; POST needs sudo; DELETE adminGET: size on disk, table count and open connections, read as the tenant login. POST {rotate: true} replaces the login's password on the engine and in the store and returns the new strings revealed. DELETE drops the database and its login
/api/db/pg · /api/db/mysqlGET/POSTdatabases.query (admin in the built-in roles); machine token acceptedThe editor. Without dbId it opens the system database as the panel's own login (the LuckPerms PostgreSQL, the gamestats MariaDB); with dbId a managed database as its own login. GET: the table list with row counts, ?table=&limit=&offset= one page of rows inside a read-only transaction (200 rows at most), ?meta=1&table= columns and primary key. POST {action}: update (exactly one row must match), insert, delete, exec (one statement, 8 s timeout, 500 rows, audited), and addColumn, dropColumn, createTable, dropTable, which answer 409 on a system engine because its schema is shared with the servers
/api/db/redisGETdatabases.queryRead-only Valkey browser: no parameters gives the overview (keyspace, role, live pub/sub channels, instances); ?pattern=&cursor=&count= scans keys with type and TTL; ?key= reads one value by type. ?target=ip:port picks a replica, ?db= a logical database
/api/system-credentialsGET/POSTadmin + sudoDerived service credentials, rotation

Joined clusters#

Capabilities from the manifest: /api/fleet reads with network.view and writes with network.edit (the handlers add admin); /api/fleet/grants is settings.secrets; /api/fleet/front-door and /api/clusters are network.view. The grant-gated routes are public at the middleware and verify the grant in the handler.

RouteMethodsAccessPurpose
/api/fleetGET/POST/PATCH/DELETErequester; admin to write; GET also answers a peer grantThe joined clusters: each peer with health, node count, inClear (an http:// address carries its credential in the clear), mesh link state, manage (what this cluster allows that one), canManage (what it allows us, learned from its answers, never set here), reach, and errors kept apart per purpose; the merged node list and provider groups. POST {url, code, label?, provider?} redeems an invite at url and links both sides, or {url, token} pastes that panel's machine token; a bare host becomes https://; probed before it is saved; a new peer starts with the mesh offered and manage: true. PATCH {id, manage?, meshLink?, label?, provider?} recomputes every grant issued to that estate from the two switches. DELETE ?id= detaches: reach withdrawn at once, replica and mesh entry dropped, grants narrowed to the node list. Peer tokens are never returned
/api/fleet/inviteGET/POST · PUTGET/POST admin (session or machine token); PUT publicPOST {label} mints a code, shown once, valid one hour, single use, stored as a hash. GET lists outstanding invites with valid / expired / spent. PUT {code, label?, url?, token?} is redeemed by a panel that holds nothing here yet: answers 200 with {ok, token, mutual} or {ok:false, error} naming expired, spent or unknown
/api/fleet/invite/checkGETpublicWhether a code is worth spending; spends nothing
/api/fleet/peerGETany grant, or the machine tokenThis cluster's node list for a joined panel (the same shape as /api/nodes?detail=1), plus publicUrl: the https cluster address once one is proven, which moves the join off a bare address
/api/fleet/meshGETgrant with the mesh cap, or the machine tokenMesh identity: the segment, and per node its public key, mesh address and candidate endpoints
/api/fleet/nodeGETgrant with the mesh capOne node's hardware detail
/api/fleet/remote-nodeGETrequesterThe asking half of the row above: ?estate=&node= fetches a far node's detail with the stored peer token, which never reaches a browser
/api/fleet/partitionGETgrant with the partition capThis store's logical partition (groups, tasks, labels, blueprints, schedules, rules, the front door, the identity projection; never sessions, grants or anything under network), with an ETag so an unchanged poll is a 304. 200 with ok:false while the store has not assigned itself an id
/api/fleet/servicesGETgrant with the manage capsThis cluster's groups, servers and instances for a cluster allowed to manage it, without container addresses. A 401 here is how the far side learns it is not allowed
/api/fleet/relayPOSTgrant with the relay cap and role: "admin"Run one request here for a joined cluster, as the person named in the signed hop: method and path from headers, body byte for byte. An explicit allow list of method and route, a deny list that wins (auth, sessions, secrets, update, every fleet route, the connector's own channel), a hop counter so a relay never relays, a 1 MiB body cap, a 120 s timeout except for streams. The person is resolved over the replicated accounts (409 while not replicated yet) and re-checked against this panel's manifest and roles; writes are audited under their name with the asking cluster in the detail, reads once per five minutes
/api/fleet/grantsGETadmin (session or machine token)Every credential this cluster issued to another panel: label, url, role, caps, created, lastUsedAt ("never used" marks a leftover) and a fingerprint. Never the secret
/api/fleet/grants/{id}DELETEadmin session plus sudo, or the machine tokenRevoke one grant; the far panel loses it on its next request
/api/fleet/front-doorGETrequesterEvery hostname the fleet publishes for failover: owner, address, who serves it now and since when, and for a foreign door whether this cluster could stand in (holds the credentials, holds a certificate, how long the owner has been silent or answering, the last probe error). sharing and sharedWith for this cluster's own zone
/api/clustersGETrequesterThis cluster first, then every mesh-linked joined cluster: id, label, local, reachable (it has issued us reach) and its panel url. Cheap enough for a page to poll instead of the whole state tree

Services & consoles#

RouteMethodsAccessPurpose
/api/services/[vmid]/consoleGET/POSTsessionRead recent lines / send a command
/api/services/[vmid]/console/streamGET (SSE)sessionLive console stream
/api/services/[vmid]/install-logGET (SSE)sessionProvisioning log during install
/api/services/[vmid]/commandsGETsessionThe server's real command list (feeds completion)
/api/services/[vmid]/completePOSTsessionLive tab completion — answered by the running server
/api/services/[vmid]/config · files · tps · agent · shareGET/POSTsessionConfig drift, file browser, TPS history, console sharing
/api/services/[vmid]/statsGETsessionLive vitals from inside the container (cpu, memory, disk, net, load, procs) plus history — the last half hour of heartbeats, so a graph has shape before its first poll. Memory excludes reclaimable page cache. 404 where the container has no agent — the RRD graphs cover that case
/api/services/[vmid]/logsGET (SSE)sessionThe service's systemd journal, relayed from the container agent. ?unit= is OPTIONAL and defaults to whatever that container's own agent runs — naming one means knowing whether it is a Paper server, a Postgres or an nginx. 404 without an agent
/api/ctagentGETsessionContainer-agent coverage across the fleet, answered from heartbeats already in memory — no container is touched. Names the silent, stale and outdated rather than averaging them
/api/ctagent/heartbeatPOSTper-container tokenInbound from a container agent. The token is derived from the vmid, so a container can only speak for itself
/api/agentsGETadminEvery node and container agent, probed live: what answers, in how many ms, what version and what source hash against what this panel ships, and why a container has no agent. ?refresh=1 skips the 20-second cache
/api/agentsPOSTadmin{op:"converge"|"restart", vmid}. Converge writes the bundled agent and restarts it, always forced — the button exists for when the version agrees and something is still wrong. Restart goes through pct, since the agent's unit allowlist excludes itself. Node agents are not drivable here: install/ owns those

Players & gameplay#

RouteMethodsAccessPurpose
/api/players (+ [id])GETsessionLive network players; Player 360 profile (rank, balance, punishments, friends, session history)
/api/bans · /api/friends · /api/parties · /api/logs · /api/blossomsGET/POSTmachine + sessionPunishments, social graph, action logs, currency
/api/reports · /api/reportGET/PATCHsessionIn-game report inbox + operator report export
/api/moderation surfaces via /api/bansModeration UI is backed by the bans API
/api/luckperms/status · groups · groups/[name] · tracks · users · users/[uuid] · permissionsGET/POST/DELETEplayers.view / players.permissions; writes refuse an account scoped to particular servers; ?estate=<id> relays to the cluster whose PostgreSQL holds the tablesStorage state, groups and their nodes, tracks, player search (?q=&limit=&offset=), a player's nodes and primary group, known permissions for completion. Every write is followed by lp networksync on a live server and answers synced
/api/luckperms/installGET/POSTplatform.install; unscoped; never relayedThe managed LuckPerms set as targets, and its install: restarts every Paper and Velocity server in the set
/api/luckperms/effectiveGETmachine path (connector token)Effective prefix and suffix for a uuid, for servers that run the connector without the plugin
/api/packs (+ [id], latest)GET/POST/PATCH/DELETEsession · download publicResource packs: upload, scope/targets, GitHub auto-pull, stable client URL
/api/voiceGET/POSTsessionManaged voice: routing mode, proxies, halls, endpoints
/api/worlds (+ coverage, pull)GET/POSTsession · pull machinePregeneration control, on-disk coverage map, shard world transfer
/api/mapsGET/PATCH/DELETEsession · delete non-viewerThe map library — worlds authored in-game with /conduit map; toggle rotation, de-list (the S3 tarball stays)
/api/languages (+ [id], lang-data)GET/POST/PATCHsession · lang-data machineLocalization catalogs and the connector feed
/api/plugins (+ [id]) · /api/plugin-bundlesGET/POST/PATCH/DELETEsessionManaged plugins (Modrinth/GitHub/URL) and config bundles
/api/leaderboards · /api/minigames/syncGET/POSTsessionGame stats surfaces

Observability & automation#

RouteMethodsAccessPurpose
/api/uptime · /api/monitorGET/POST/PATCH/DELETEsessionHealth history + custom monitors (tcp/udp/http/ping)
/api/alerts · /api/incidentsGETsessionAlert feed (open per entry + openCount; see below); incidents with postmortem markdown export
/api/status (+ subscribe) · /api/badge/[slug]GET/POSTpublic (opt-in)Public status page, email subscriptions, embeddable badges
/api/metrics (+ history) · /api/metrics/prometheusGETsession · bearerLive metrics; Prometheus scrape endpoint
/api/analytics · /api/forecast · /api/pulse · /api/rightsizing · /api/digest · /api/profilerGET/POSTsessionAnalytics, capacity forecast, fleet pulse, rightsizing, digests, spark profiler
/api/activity · /api/op-audit · /api/audit (+ erase) · /api/siemGET/POSTsession · adminActivity feed, operator audit, player audit + GDPR erasure, SIEM export
/api/schedules (+ [id]) · /api/automationGET/POST/PATCH/DELETEsessionCron actions with warn countdowns; self-healing rules
/api/maintenance-windows · /api/event-windowsGET/POST/PATCH/DELETEsessionPlanned downtime and event windows (countdowns, MOTD swaps)
/api/hooks/[token]POSTtoken in URLInbound webhooks (restart/broadcast/scale/reconcile), HMAC-verifiable
/api/outbound-hooks · /api/webhooksGET/POST/PATCH/DELETEadminSigned outbound event subscriptions + inbound hook management
/api/mail (+ announce)GET/POSTadmin + sudoSMTP settings, DKIM, announce mail
/api/pushGET/POSTsessionWeb-push subscriptions + test
/api/search · /api/liveGETsession · SSEGlobal entity search; live update bus
/api/askGET/POST/PUTsession · key PUT adminAsk Conduit — one question answered from a bundle of the panel's own evidence (read-only); PUT stores the Anthropic key after validating it
/api/hubGET/PUTsession · PUT adminWhich build this panel runs and what the hub offers (?fresh=1 skips the 15-minute memo); PUT sets the hub URL — validated by reading its feed — and the telemetry switch
/api/updateGET/POSTsession · POST adminThe panel updating itself: GET merges the leader's preflight with the run persisted in the store. POST {action:"install"} is the button — it runs the checks and rolls out automatically once they pass, so the rollout cannot be reached without them. preflight and run remain for scripts; a bare run still needs an all-green preflight under five minutes old for the version the feed currently offers
/api/store-history (+ [rev], revert)GET/POSTsession · revert adminConfig history: per-revision snapshots with human diffs and actors (secrets masked to sentinels at capture); revert goes through the CAS write path and re-resolves sentinels to live values
/api/driftGETsessionThe drift report — reconcile actions normalised to their kind and counted per day; a fix that recurs daily is a bug wearing a bandage
/api/secretsGET/POSTsession only; settings.secretsThe register: every credential by store path with presence, count, an eight-character fingerprint, what it protects, its blast radius and rotation class, never a value. posture.machineAuth is enforced or opted-out; sharedWith names the joined clusters whose reach token can read all of it. Answers 409 instead of a list of absences when the store read is stale or empty. POST {id} records that an external secret was rotated; a self-service one is refused
/api/services/[vmid]/regionsGETsessionFolia region view: per-player placement (world, chunk, region id) inferred from scheduler threads, grouped per region with chunk bounds
/api/nodes/[node]GET/PATCHGET requester; ?cluster=<id> reads a joined cluster's node through its own Proxmox over the mesh (admin or operator, unscoped, only while that cluster has issued reach); PATCH adminNode detail as {ok, detail}, the same envelope /api/fleet/node and /api/fleet/remote-node use. PATCH arms or ends maintenance (an object arms a window, null ends it, anything else is a 400 — truthiness once re-armed a window on false) or sets {provider}; both answer 409 for a joined cluster's node, which is set on its own panel
/api/replica-coverageGETrequester; nodes.viewWhether every node has a panel and a fallback exists, as the reconciler sees it right now, plus what it would do about it. 200 with ok:false when the node list could not be read

/api/alerts: read open, not resolved. resolved is a property of one ENTRY, not of the condition. A "service is DOWN" record keeps resolved: false for ever — it is a true record of something that happened — and its recovery arrives as a separate entry carrying the same key. Openness is therefore "is the newest entry for this key unresolved", which is what the open flag on each entry and the openCount on the response answer, both judged over the whole ring rather than the returned window. Filtering on !resolved reports a healthy cluster as a wall of red; that is not hypothetical, it is how this note came to be written.

Streams (SSE)#

StreamEmits
/api/live?topics=…Version bumps per topic (state, activity, bans, logs, audit, opaudit, alerts, ssh-approvals, hub, conn) — clients refetch on change. conn fires when a heartbeat's player geometry (region/chunk) actually changes, which is what makes the regions view pushed rather than polled. One multiplexed EventSource per browser — six streams once starved every fetch on the page
/api/services/[vmid]/console/streamBase64 console frames (agent WebSocket bridge, SSH-poll fallback)
/api/services/[vmid]/install-logLive provisioning output

Machine API#

Paths intended for the connector and infrastructure (bearer-token auth; see lib/connector-auth.ts): /api/connector/* (heartbeat, register, unregister, event, announce, broadcasts, tabresult, transfer, pending, report, maintenance, rounds, servers, move-targets, selection — the in-game wand posts its box here — and maps/groups|upload|commit, the map studio's upload path: the connector asks for the game groups, gets a presigned S3 PUT for the world tarball, and commits only after the panel has verified the object exists), plus /api/luckperms/effective, /api/worlds/pull, /api/languages/lang-data, /api/friends|parties|bans|logs ops, and /api/metrics/prometheus.

The hub speaks its own small protocol, documented in hub/README.md: GET /v1/feed, GET /v1/artifacts/<version>.tgz, PUT /v1/releases/<version> (bearer publish token), POST /v1/ping, POST /v1/crash, GET /v1/crashes, GET /v1/stats, and GET /v1/diagnostics (a cluster reading back its own private crash detail, bearer cluster secret). The full protocol is described in architecture.md → Connector protocol.

One machine path authenticates differently: POST /api/auth/cli-session (the SSH gateway's token exchange) accepts only the CT-local secret at /etc/conduit/cli-gateway.secret, compared constant-time — useless from anywhere the reconcile leader hasn't provisioned that file.

Rate limiting — there is no global limiter. Login enforces a uniform 400ms failure delay, inbound hooks have per-hook cooldowns, and report filing is rate-limited; other routes rely on the auth gate. Front the panel with your own limiter if it faces the open internet.

Edit on GitHubdocs/api.md 5 min readUpdated