REST API
Complete HTTP reference for the Nimbus controller — routes, auth levels, payloads, and error codes.
The Nimbus controller exposes a single HTTP API served by Ktor (CIO engine) on the port configured in [api]. This reference tracks the controller shipping with nimbusVersion in gradle.properties (check the running controller at GET /api/health for the exact version — all examples below show the current stable version).
Base URL — http://{bind}:{port} (default http://127.0.0.1:8080). Set [api] bind = "0.0.0.0" in nimbus.toml to accept non-localhost traffic. The dashboard needs CORS entries in [api] allowed_origins.
Authentication model
Every route (except those tagged Public) requires a bearer token:
Authorization: Bearer <token>Five distinct auth levels are enforced by the controller:
| Level | Credential | Typical caller |
|---|---|---|
| Public | none | /api/health, /api/resourcepacks/files/{uuid}.zip |
| Cluster token | [cluster] token (separate secret) | Agents — for bootstrap, template download, state sync |
| Service token | HMAC-SHA256 derivation of the master API token | Backend SDK, Velocity Bridge |
| Bearer (master) | [api] token literal OR JWT with the admin scope | Dashboard, Remote CLI, operators |
| Admin only | master token / JWT admin only — derived service tokens are rejected | All mutating/system routes |
The master token is read from [api] token or the NIMBUS_API_TOKEN environment variable (env takes precedence and never appears in ps). If both are blank, the controller generates a random token on startup and logs the first 8 characters.
Service token derivation
Backend servers receive a token derived from the master token so they cannot call admin routes. The derivation is public knowledge, so callers that want to share tokens with low-privilege consumers can emit it:
// nimbus-core/src/main/kotlin/.../api/NimbusApi.kt
HMAC-SHA256(key = masterToken, message = "nimbus-service-token")
.toHex()JWT tokens
If [api] jwt_enabled = true, POST /api/tokens mints short-lived HS256 JWTs whose scopes gate specific route groups. The scopes recognised by the controller are defined in ApiScope.kt:
services:read services:write
groups:read groups:write
metrics:read
cluster:read cluster:write
files:read files:write
config:read config:write
audit:read
adminadmin is a super-scope that unlocks every route. Service-level routes additionally accept any of services:read, services:write, groups:read, metrics:read.
CORS, rate limits, and error shape
- CORS — origins in
[api] allowed_originsare accepted forGET/POST/PUT/DELETE/PATCHwith bothhttpandhttpsschemes. An empty list defaults to "any origin" (useful for dev, loud warning in logs for prod). - Rate limits — global
120 req/minper client IP;/api/stress/start|stop|rampfalls under a stricter5 req/minbucket. The client IP comes fromX-Forwarded-Forwhen[api] trust_forwarded_for = true, otherwise the socket address. - Error body — every non-2xx response has the shape:
{
"success": false,
"message": "Service 'Lobby-1' not found",
"error": "SERVICE_NOT_FOUND"
}error is a stable machine-readable code. See Error codes below for the full list.
Core endpoints
Health (public)
GET /api/health
Liveness probe — always public, always 200.
Response
{
"status": "ok",
"version": "0.9.1",
"uptimeSeconds": 12473,
"services": 7,
"apiEnabled": true
}Controller info
GET /api/controller/info — service
Runtime stats (JVM heap, services budget), system info, and a non-blocking update check against GitHub Releases. Used by the dashboard header.
GET /api/controller/changelog — service
Parsed changelog pulled from docs/content/docs/project/changelog.mdx on GitHub main (cached for 5 minutes). Returns { "entries": [{ "version", "title", "body" }] }.
Network status
GET /api/status — service
Aggregated network view: per-group instance counts, total players, version strings.
{
"networkName": "nimbus",
"online": true,
"uptimeSeconds": 3600,
"uptimeHuman": "1h 0m 0s",
"totalServices": 4,
"totalPlayers": 42,
"groups": [
{ "name": "Lobby", "instances": 2, "maxInstances": 5,
"players": 30, "maxPlayers": 100,
"software": "PAPER", "version": "1.21.4" }
]
}GET /api/players — service
Pings every READY backend (timeout 3 s) and aggregates online players from the sample list. This is an approximation — for authoritative tracking use the Players module's /api/players/online endpoint instead.
POST /api/players/{name}/send — service
Transfers a player to another backend via the Velocity send command. Returns 503 if no proxy is READY.
Body — { "targetService": "Lobby-2" }
POST /api/players/{name}/kick — service
Kicks a player from the network. Executed via Velocity's velocity kick command.
Body — { "reason": "AFK" }
POST /api/broadcast — service
Broadcasts a message to every READY service (optionally scoped to a group). Uses velocity broadcast on proxies, say on backends.
Body — { "message": "Restarting in 5 min", "group": "Lobby" }
Services
All routes are service-level unless marked otherwise.
Listing
GET /api/services?group=&state=&customState=
Filter by group name, ServiceState (PREPARING/PREPARED/STARTING/READY/DRAINING/STOPPING/STOPPED/CRASHED), or the SDK-settable customState string.
GET /api/services/health
Aggregate health summary — total/ready/healthy counts, average TPS across READY, memory totals.
GET /api/services/{name}
Full ServiceResponse including memory (resolved from /proc on Linux, tasklist on Windows), TPS, sync sub-object when state-sync is enabled.
GET /api/services/{name}/metrics/history?minutes=60
Historical memory + player samples from the service_metric_samples table. minutes is clamped to 5..1440 and up to 2000 samples are returned.
Lifecycle
| Method | Path | Notes |
|---|---|---|
POST | /api/services/{groupName}/start | Starts a new instance of the group (note: path uses the group name, not a service name). 201 on success, 409 if max instances reached. |
POST | /api/services/{name}/stop | Graceful stop. |
POST | /api/services/{name}/restart | Stop + start. |
POST | /api/services/{name}/migrate | Move to another node. Body { "target": "worker-1" }, omit/null to let the placement strategy choose. |
POST | /api/services/{name}/exec | Body { "command": "say hi" }. Writes to the process stdin. |
SDK endpoints
Backends (using :nimbus-sdk) call these:
| Method | Path | Body |
|---|---|---|
PUT | /api/services/{name}/state | { "customState": "waiting" } — requires service to be READY. |
GET | /api/services/{name}/state | Current custom state. |
PUT | /api/services/{name}/health | { "tps": 19.8 } — memory fields in the request are accepted but ignored (the controller reads /proc). |
PUT | /api/services/{name}/players | { "playerCount": 17 } |
GET | /api/services/{name}/logs?lines=100 | Tails logs/latest.log (lines clamped 1..1000). |
POST | /api/services/{name}/message | { "from": "Lobby-1", "channel": "queue", "data": { ... } }. Emits a SERVICE_MESSAGE event. The special target controller is virtual (no service lookup). |
Groups
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /api/groups | service | List with activeInstances and scanned modIds. |
GET | /api/groups/{name} | service | Single group. |
POST | /api/groups | service | Create. Writes config/groups/{name}.toml + reloads. |
PUT | /api/groups/{name} | service | Replace. Immutable name. |
DELETE | /api/groups/{name} | service | Deletes TOML. Rejects with 409 if any instances are running. |
Create/update body (flat translation layer — the TOML on disk is nested):
{
"name": "Lobby", "type": "LOBBY", "template": "lobby",
"software": "PAPER", "version": "1.21.4",
"modloaderVersion": "", "jarName": "", "readyPattern": "",
"memory": "2G", "maxPlayers": 100,
"minInstances": 1, "maxInstances": 5,
"playersPerInstance": 50, "scaleThreshold": 0.8, "idleTimeout": 300,
"stopOnEmpty": true, "restartOnCrash": true, "maxRestarts": 3,
"jvmArgs": [], "jvmOptimize": true
}Validation rejects names outside ^[a-zA-Z0-9_-]{1,64}$, invalid software/type enums, memory not matching ^\d+[MmGg]$, version not matching ^\d+\.\d+(\.\d+)?(-.*)?$, or ranges outside minInstances <= maxInstances, scaleThreshold ∈ [0,1].
Dedicated services — admin
Single-instance, fixed-port services under paths.dedicated/<name>/.
| Method | Path | Notes |
|---|---|---|
GET | /api/dedicated | List with runtime status. |
GET | /api/dedicated/{name} | Detail. |
POST | /api/dedicated | Create — auto-provisions the directory. |
PUT | /api/dedicated/{name} | Replace config (name immutable). |
DELETE | /api/dedicated/{name} | Delete config + TOML. |
POST | /api/dedicated/{name}/start | |
POST | /api/dedicated/{name}/stop | |
POST | /api/dedicated/{name}/restart | |
POST | /api/dedicated/{name}/modpack/import | Import a CurseForge or Modrinth modpack into the dedicated dir. |
POST | /api/dedicated/{name}/modpack/upload | Upload a server-pack zip. |
Port conflict — the create endpoint refuses a port already claimed by another dedicated config with DEDICATED_PORT_IN_USE. Dynamic groups allocate their ports from backend_port_start and won't collide with dedicated ports unless the operator configures overlapping ranges.
Maintenance — service
Global and per-group maintenance flag plus a username/UUID whitelist surfaced to Velocity for login-time enforcement.
| Method | Path | Body / notes |
|---|---|---|
GET | /api/maintenance | Full status. |
POST | /api/maintenance/global | { "enabled": true, "reason": "patching" } |
PUT | /api/maintenance/global | { "motdLine1", "motdLine2", "protocolText", "kickMessage" } (all optional) |
POST | /api/maintenance/groups/{name} | { "enabled": true, "reason": "" } |
PUT | /api/maintenance/groups/{name} | { "kickMessage": "..." } |
POST | /api/maintenance/whitelist | { "entry": "Alice" } — name or UUID. |
DELETE | /api/maintenance/whitelist | { "entry": "Alice" } |
Proxy sync — service
Reads and writes config for the Velocity Bridge (MOTD, tablist, chat, player tab overrides).
| Method | Path | Notes |
|---|---|---|
GET | /api/proxy/config | Unified read used by the Bridge on connect. Includes maintenance snapshot. |
GET / PUT | /api/proxy/tablist | Tablist header/footer/playerFormat/updateInterval. |
GET / PUT | /api/proxy/motd | MOTD lines + maxPlayers + offset. |
GET / PUT | /api/proxy/chat | Chat format + enabled. |
GET | /api/proxy/tablist/players | All UUID overrides. |
PUT | /api/proxy/tablist/players/{uuid} | { "format": "&7[Afk] {name}" } |
DELETE | /api/proxy/tablist/players/{uuid} | Clear override. |
POST /api/proxy/events — service
Bridge reports player lifecycle events here. The controller validates the UUID format (strict parse, 400 on malformed input) and emits the matching NimbusEvent.
Body
{ "type": "PLAYER_CONNECTED", "player": "Alice",
"uuid": "11111111-2222-3333-4444-555555555555", "service": "Lobby-1" }Accepted types: PLAYER_CONNECTED, PLAYER_DISCONNECTED, PLAYER_SERVER_SWITCH (adds fromService/toService).
Commands — service
The Velocity Bridge turns this into dynamic in-game commands.
| Method | Path | Notes |
|---|---|---|
GET | /api/commands | Every console command with a non-empty permission node (metadata + subcommands + completion hints). |
POST | /api/commands/{name}/execute | { "args": ["sub", "arg"] } — runs the command, captures typed output lines. |
Response lines are tagged header / info / success / error / item / text so Bridge can colour them consistently.
Files — admin
Scopes: templates (full rw), services (full rw), groups (read-only).
| Method | Path | Notes |
|---|---|---|
GET | /api/files/{scope}/{path...} | List directory or read file. |
PUT | /api/files/{scope}/{path...} | Overwrite file contents (JSON body). |
POST | /api/files/{scope}/{path...} | Create directory or upload file (multipart). Max upload 100 MB. |
DELETE | /api/files/{scope}/{path...} | Delete file or empty directory. |
Path traversal attempts (.., absolute paths) are rejected with PATH_TRAVERSAL. The groups scope is read-only — use the Groups CRUD API for edits.
Config — admin
GET /api/config
Read the current nimbus.toml sans secrets (hasToken: boolean instead of the token itself).
PATCH /api/config
Partial update of non-critical fields — networkName, consoleColored, consoleLogEvents. Any other field requires a restart and direct TOML edit. The controller atomically rewrites nimbus.toml preserving other sections.
System — admin
| Method | Path | Notes |
|---|---|---|
POST | /api/reload | Hot-reloads every config/groups/*.toml. Emits CONFIG_RELOADED. Returns a ReloadReport (v0.12.0+). |
POST | /api/shutdown | Responds 202, then System.exit(0) after 250 ms so the response flushes. Essential for daemon deployments without a TTY console. |
Reload report
Since v0.12.0, /api/reload returns a structured ReloadReport that names every known config section, its reload scope (LIVE / NEXT_SERVICE_PREPARE / REQUIRES_RESTART), and whether the current pass applied it. requiresRestartIfChanged is a reference list of sections whose changes would require a full controller restart — not a per-request delta. Backwards-compatible: the legacy success, groupsLoaded, and message fields are retained for older dashboard versions.
{
"success": true,
"groupsLoaded": 3,
"message": "Reloaded 3 group config(s)",
"sections": [
{ "name": "groups", "scope": "LIVE", "applied": true, "description": "Group definitions..." },
{ "name": "sandbox", "scope": "NEXT_SERVICE_PREPARE","applied": false, "description": "Global sandbox defaults..." },
{ "name": "api", "scope": "REQUIRES_RESTART", "applied": false, "description": "API bind/port/token/JWT/CORS..." }
],
"requiresRestartIfChanged": ["api", "database", "cluster", "audit", "metrics", "controller", "network", "loadbalancer", "punishments", "resourcepacks", "dashboard", "console"],
"warnings": []
}Doctor — admin
GET /api/doctor
Runs every registered health check (environment, database, cluster, modules' own checks). Response mirrors DoctorReport:
{
"sections": [
{ "name": "Environment",
"findings": [{ "level": "OK", "message": "...", "hint": null }] }
],
"warnCount": 0, "failCount": 0, "status": "ok"
}HTTP status is always 200 — switch on status (ok / warn / fail) or failCount yourself.
Cluster & load balancer — admin
GET /api/nodes
Every cluster node with system sub-object (hostname, OS/arch, CPU model/load, system memory, Java version/vendor) plus per-node service lists.
GET /api/loadbalancer
Backend health, active connection counts, strategy, total/rejected connection counters. 404 with LOAD_BALANCER_NOT_ENABLED when the LB isn't configured.
Cluster bootstrap — cluster token
GET /api/cluster/bootstrap
Returns the controller's TLS certificate material so agents can pin it before the first wss:// handshake. Gated by the cluster token (Authorization: Bearer <clusterToken>), not the REST API token.
Response
{
"fingerprint": "SHA256 AB:CD:...",
"certPem": "-----BEGIN CERTIFICATE-----\n...",
"wsUrl": "wss://controller.example.net:8443/cluster",
"validUntil": "2027-04-15T00:00:00Z",
"sans": ["controller.example.net", "10.0.0.5"]
}This endpoint is reachable over plain HTTP on the REST port on purpose — otherwise agents could not trust TLS yet. The response body is public-key material (not secret), but the cluster token gates access to prevent information disclosure.
State sync — cluster token
Agents pull and push the controller's canonical copy of a service's working directory. Auth via Authorization: Bearer <clusterToken> or the legacy ?token=<clusterToken> query param.
| Method | Path | Purpose |
|---|---|---|
GET | /api/services/{name}/state/manifest | Current StateManifest JSON. |
GET | /api/services/{name}/state/file/{path...} | Single file raw bytes. |
POST | /api/services/{name}/state/sync | Multipart push: manifest form part + one file:<relpath> part per upload. Returns 409 if another push is in-flight for the same service. Atomic commit on success; StateSyncResponse body. |
Template download — cluster token
| Method | Path | Notes |
|---|---|---|
GET | /api/templates/{name}/download?token=&software= | Returns a ZIP of the group template plus applicable global*/ overlays. Auth is ?token=<clusterToken> query param. |
GET | /api/templates/{name}/hash?token=&software= | SHA-256 of the same file set so agents can skip unchanged downloads. |
Tokens — admin
| Method | Path | Notes |
|---|---|---|
POST | /api/tokens | { "subject": "...", "scopes": ["services:read"], "expiresInSeconds": 86400 } — 201 with HS256 JWT. Requires [api] jwt_enabled = true. Rejects expiry < 60 s and unknown scopes. |
GET | /api/tokens/scopes | Sorted list of every scope the controller accepts. |
Modules — admin
| Method | Path | Notes |
|---|---|---|
GET | /api/modules | Every loaded module plus any modules/*.jar present on disk but not installed. Includes the module's dashboard manifest. |
POST | /api/modules/install/{id} | Extract an embedded module from the fat JAR. 201 if installed (restart required). |
POST | /api/modules/uninstall/{id} | Removes the JAR. Restart required to take effect. |
Software & plugins — admin
| Method | Path | Notes |
|---|---|---|
GET | /api/software | All ServerSoftware enum values with capability flags. |
GET | /api/software/{type}/versions | Stable + snapshot version lists per software (Paper, Velocity, Purpur, Folia, Pufferfish, Leaf, Forge, NeoForge, Fabric). |
GET | /api/software/{type}/modloader-versions?mcVersion= | Forge/NeoForge per-MC loader versions; Fabric loader list. |
GET | /api/plugins/search?q=&mcVersion=&platform= | Multi-source (Hangar + Modrinth) search — returns source, name, author, slug, projectId, description, downloads. |
POST | /api/plugins/install | { "source", "slug", "projectId", "group", "mcVersion", "platform" } — downloads the plugin into the target group's template/plugins dir, resolving dependencies. |
Modpacks — admin
| Method | Path | Notes |
|---|---|---|
POST | /api/modpacks/resolve | Look up a pack by URL or ID. |
POST | /api/modpacks/import | Import by manifest (CurseForge/Modrinth). |
POST | /api/modpacks/upload | Single-shot upload of a server pack (raw body, filename query param). |
POST | /api/modpacks/upload/init | Start a chunked upload — returns an upload ID. |
POST | /api/modpacks/upload/chunk | Append a chunk. |
POST | /api/modpacks/upload/finalize | Commit the assembled pack. |
The chunked flow exists because browsers struggle to stream single large multipart bodies through the dashboard's auth layer. Native tools can use the single-shot POST /api/modpacks/upload instead.
Stress tests
| Method | Path | Auth | Notes |
|---|---|---|---|
GET | /api/stress | admin | Status — subject to the global 120/min limit. |
POST | /api/stress/start | admin | Body { "players": 100, "group": "Lobby", "rampSeconds": 30 }. Rate limited 5/min. |
POST | /api/stress/stop | admin | 5/min. |
POST | /api/stress/ramp | admin | { "players": 200, "durationSeconds": 60 }. 5/min. |
Audit log — admin
GET /api/audit?limit=50&offset=0&action=&actor=
Paged dump of the audit_log table. limit clamped 1..500. Filters are optional exact-match strings.
Metrics — service
GET /api/metrics
Prometheus text exposition — nimbus_info{version}, nimbus_uptime_seconds, nimbus_services_total, nimbus_services_by_state{state}, nimbus_services_by_group{group}, nimbus_players_total, plus node, load balancer, and state sync gauges. Requires a service- or master-level token. Configure your scraper with a bearer token header.
Added in v0.12.0:
| Metric | Type | Labels | Description |
|---|---|---|---|
nimbus_warmpool_size | gauge | group | Currently prepared services in the warm pool for this group. |
nimbus_warmpool_target | gauge | group | Configured scaling.warm_pool_size target. |
nimbus_service_crashes_total | counter | group | Service crashes since controller start. Group label is inferred from the <Group>-<N> naming convention; dedicated services tally under their own name. |
nimbus_scaling_events_total | counter | group, direction=up|down | Scaling decisions emitted by the engine. |
nimbus_placement_blocked_total | counter | group | Starts blocked by placement constraints (pinned node offline etc.). |
Counters reset on controller restart — this is intentional. Prometheus rate() and increase() handle counter resets natively, and long-running trends are already in the DB via MetricsCollector.
Remote CLI console — admin
The Remote CLI uses two endpoints on this block.
| Method | Path | Notes |
|---|---|---|
POST | /api/console/complete | Tab completion — { "buffer": "ser", "cursor": 3 }. Returns candidate list. |
WS | /api/console/stream | Multiplexed command/screen/events. See WebSocket reference. |
Module routes
Loaded modules inject their own routes under the appropriate auth block. Every route below assumes the module's JAR is present in modules/ or embedded.
Players module — service
| Method | Path | Notes |
|---|---|---|
GET | /api/players/online | Canonical online roster from the Bridge-fed tracker. |
GET | /api/players/online/{uuid} | Single live session. |
GET | /api/players/history/{uuid}?limit=20 | Session history rows. |
GET | /api/players/info/{uuid} | Player meta + online flag + current service. |
GET | /api/players/all?q=&limit=50 | Search or recent listing across every known player. |
GET | /api/players/stats | { "online", "totalUnique", "perService" }. |
The core GET /api/players (no module) pings backends live via Server List Ping. The Players-module endpoints above are the source of truth when you care about history — the core endpoint is only a fallback when the module isn't loaded.
Permissions module — service
Route block /api/permissions/*. Summary table:
| Method | Path | Purpose |
|---|---|---|
GET / POST | /groups | List / create |
GET / PUT / DELETE | /groups/{name} | Read / update / delete |
POST / DELETE | /groups/{name}/permissions | Add / remove perms (context server, world, expiresAt) |
GET / PUT | /groups/{name}/meta | Group meta (read list, set key) |
DELETE | /groups/{name}/meta/{key} | Remove a meta key |
GET | /players?q=&limit=50 | Search known players |
GET | /players/{uuid}?server=&world= | Player perms + effective permissions + display |
PUT | /players/{uuid}?server=&world= | Register / update (called on proxy join) |
POST / DELETE | /players/{uuid}/groups | Add / remove a group (context in body) |
GET / PUT | /players/{uuid}/meta | Player meta |
DELETE | /players/{uuid}/meta/{key} | Remove meta key |
GET | /check/{uuid}/{permission...}?server=&world= | Plain check → { allowed }. |
GET | /debug/{uuid}/{permission...}?server=&world= | Check with full inheritance chain + explanation. |
GET | /tracks | List tracks. |
GET / DELETE | /tracks/{name} | Read / delete a track. |
POST | /tracks | Create track { "name", "groups": ["rookie","mod","admin"] }. |
POST | /tracks/{name}/promote/{uuid} | Move player one step up. |
POST | /tracks/{name}/demote/{uuid} | Move player one step down. |
POST | /bulk/permissions | Add one permission to many groups. |
POST | /bulk/groups | Assign one group to many players. |
GET | /audit?limit=50&offset=0 | Perms-specific audit log. |
Display module — service
| Method | Path | Notes |
|---|---|---|
GET | /api/displays | All display configs. |
GET | /api/displays/{name} | Config for a group. |
PUT | /api/displays/{name} | Partial update (sign lines, NPC subtitle/items/inventory, state labels). |
POST | /api/displays/{name}/reset | Reset to defaults for the group. |
GET | /api/displays/{name}/state/{state} | Resolve a state label (for sign rendering). |
Scaling module — service
All under /api/scaling/*:
| Method | Path | Notes |
|---|---|---|
GET | /status | Groups with active schedule rule + 10 most recent decisions. |
GET | /schedules | Every group's schedule config (rules, warmup). |
GET | /schedules/{group} | Per-group schedule. |
GET | /history/{group}?hours=24 | Player count snapshots. |
GET | /predictions/{group} | Next 6 hours, predicted players + sample count. |
GET | /decisions?limit=50 | Recent ScalingDecisions rows. |
Punishments module — service
| Method | Path | Notes |
|---|---|---|
GET | /api/punishments?active=true&type=&limit=&offset= | List. |
POST | /api/punishments | Issue — resolves UUID via explicit/Mojang API. See below. |
GET | /api/punishments/{id} | Single record. |
DELETE | /api/punishments/{id} | Revoke — body { "revokedBy", "reason" }. |
GET | /api/punishments/player/{uuid}?limit=100 | Full history. |
GET | /api/punishments/check/{uuid}?ip=&group=&service= | Fast proxy login check — returns a pre-rendered kickMessage. |
GET | /api/punishments/mute/{uuid}?group=&service= | Scoped mute check. |
GET / PUT | /api/punishments/messages | Read / replace messages.toml templates. |
Issue body
{
"type": "TEMPBAN",
"targetName": "Alice",
"targetUuid": null,
"targetIp": null,
"duration": "7d",
"reason": "griefing",
"issuer": "console",
"issuerName": "Moderator",
"scope": "NETWORK",
"scopeTarget": null
}Types TEMPBAN / TEMPMUTE require duration (parsed by DurationParser, permanent values rejected). IPBAN requires targetIp. Non-NETWORK scopes require scopeTarget. If targetUuid is blank and targetName is not UUID-shaped, the controller calls Mojang's name → UUID API — expect a 404 on unknown names.
Resource packs module
Split across service-authed and public:
Service-authed — /api/resourcepacks/*
| Method | Path | Notes |
|---|---|---|
GET | / | List registered packs. |
GET | /{id} | Single pack. |
POST | / | Register a URL pack — body { "name", "url", "sha1Hash" (40 hex chars, required), "promptMessage", "force" }. |
POST | /upload?name=&force=&prompt= | Raw-body upload (octet-stream). SHA-1 computed while streaming. |
DELETE | /{id} | Delete pack + local file if present. |
GET | /assignments?packId= | All assignments (or filtered by pack). |
POST | /{id}/assignments | Body { "scope": "<GLOBAL|GROUP|SERVICE>", "target": "Lobby", "priority": 10 }. |
DELETE | /assignments/{id} | Remove a single assignment. |
GET | /for-group/{group}?service= | Plugin-facing resolved stack in apply order. |
POST | /status | Backend telemetry — { "playerUuid", "packUuid", "status" }. |
Public — GET /api/resourcepacks/files/{uuid}.zip
This is the only authenticated-by-hash endpoint. Minecraft clients cannot send bearer tokens, so tamper resistance comes from the SHA-1 hash negotiated during setResourcePack(). Treat the URL itself as non-secret — anyone who knows the UUID can download, but cannot substitute a different file without breaking the hash check.
Backup module — admin
Backup archives can contain world data, secrets, and DB dumps. Every route is admin-only.
| Method | Path | Notes |
|---|---|---|
GET / PUT | /api/backups/config | Live read / rewrite of config/modules/backup/backup.toml. PUT validates + hot-reloads the scheduler. |
GET | /api/backups?target=&status=&limit=&offset= | List (limit clamped 1..1000). |
GET | /api/backups/schedules | Scheduler view of cron entries. |
GET | /api/backups/status | { "activeJobs", "localDestination", "schedules" }. |
GET | /api/backups/{id} | Record. |
GET | /api/backups/{id}/manifest | Streams the trailing MANIFEST.sha256 entry. |
GET | /api/backups/{id}/download | Raw tar+zstd archive (application/zstd). |
POST | /api/backups/trigger | Body { "targets": ["services","config"], "scheduleClass": "manual", "target": "Lobby" }. 201 with the records created. |
POST | /api/backups/{id}/restore | { "targetPath": "/path", "dryRun": false, "force": false }. Returns filesExtracted. |
POST | /api/backups/{id}/verify | Re-hashes the archive, compares against MANIFEST.sha256. |
DELETE | /api/backups/{id} | Remove record + archive. |
POST | /api/backups/prune | { "dryRun": false, "retentionClass": "DAILY" } — GFS prune. |
Valid target strings: services, dedicated, templates, config, state_sync, database. Retention classes: HOURLY, DAILY, WEEKLY, MONTHLY, MANUAL.
Error codes
Every non-2xx body carries an error field. The canonical set lives in api/ApiErrors.kt as the ApiError enum. Each entry is paired with a default HTTP status.
Auth — AUTH_FAILED, UNAUTHORIZED, FORBIDDEN, READ_ONLY, AUTH_CHALLENGE_INVALID, AUTH_RATE_LIMITED, AUTH_DISABLED, AUTH_SESSION_INVALID, AUTH_SESSION_EXPIRED, AUTH_PLAYER_OFFLINE, AUTH_TOTP_REQUIRED, AUTH_TOTP_INVALID, AUTH_TOTP_ALREADY_ENABLED, AUTH_MAGIC_LINK_INVALID, AUTH_LOGIN_CHALLENGE_EXPIRED
Generic — VALIDATION_FAILED, INTERNAL_ERROR, PAYLOAD_TOO_LARGE, NO_FIELDS_TO_UPDATE
Service — SERVICE_NOT_FOUND, SERVICE_NOT_READY, SERVICE_UNAVAILABLE, SERVICE_START_FAILED, SERVICE_STOP_FAILED, SERVICE_RESTART_FAILED
Group — GROUP_NOT_FOUND, GROUP_ALREADY_EXISTS, GROUP_HAS_RUNNING_INSTANCES
Dedicated — DEDICATED_NOT_FOUND, DEDICATED_ALREADY_EXISTS, DEDICATED_ALREADY_RUNNING, DEDICATED_DIRECTORY_NOT_FOUND, DEDICATED_PORT_IN_USE
Command — COMMAND_NOT_FOUND, COMMAND_NOT_REMOTE, COMMAND_EXECUTION_FAILED
Stress — STRESS_ALREADY_RUNNING, STRESS_NOT_RUNNING
Cluster / LB — CLUSTER_NOT_ENABLED, CLUSTER_TOKEN_MISSING, LOAD_BALANCER_NOT_ENABLED, NODE_NOT_FOUND
Files — INVALID_SCOPE, PATH_NOT_FOUND, PATH_TRAVERSAL
Proxy — PROXY_NOT_AVAILABLE
Template — TEMPLATE_NOT_FOUND
Modpack — MODPACK_NOT_FOUND, MODPACK_INVALID, MODPACK_UPLOAD_FAILED, CHUNKED_UPLOAD_NOT_FOUND, CHUNKED_UPLOAD_INVALID, CURSEFORGE_API_KEY_MISSING
Plugin — PLUGIN_VERSION_NOT_FOUND
Software — SOFTWARE_UNKNOWN
Scaling — SCALING_CONFIG_NOT_FOUND
Players — PLAYER_NOT_FOUND, PLAYER_NOT_ONLINE
Perms — PERMISSION_GROUP_NOT_FOUND, PERMISSION_TRACK_NOT_FOUND
Display — DISPLAY_CONFIG_NOT_FOUND
Punishments — PUNISHMENT_NOT_FOUND, PUNISHMENT_ALREADY_REVOKED, PUNISHMENT_TARGET_INVALID, PUNISHMENT_DURATION_INVALID
Resource packs — RESOURCE_PACK_NOT_FOUND, RESOURCE_PACK_ALREADY_EXISTS, RESOURCE_PACK_INVALID_URL, RESOURCE_PACK_UPLOAD_FAILED, RESOURCE_PACK_ASSIGNMENT_NOT_FOUND
Backup — BACKUP_NOT_FOUND, BACKUP_ARCHIVE_MISSING, BACKUP_MANIFEST_MISSING, BACKUP_IN_PROGRESS, BACKUP_RESTORE_FAILED, BACKUP_VERIFICATION_FAILED, BACKUP_CONFIG_INVALID
Removed / renamed in 0.13: INSUFFICIENT_SCOPE was unused and has been removed. INVALID_INPUT and NOT_FOUND are deprecated — every endpoint now returns a domain-specific code (e.g. BACKUP_NOT_FOUND, PERMISSION_GROUP_NOT_FOUND, or VALIDATION_FAILED). The ApiErrors compat facade still exposes the old string constants unchanged, so any out-of-tree caller switching on "NOT_FOUND" / "INVALID_INPUT" keeps working; the facade is removed in 0.14.
Quick reference — auth matrix
| Route prefix | Auth |
|---|---|
/api/health | public |
/api/metrics | service |
/api/resourcepacks/files/{uuid}.zip | public (hash-signed) |
/api/cluster/bootstrap | cluster token |
/api/templates/{name}/download, /hash | cluster token (query) |
/api/services/{name}/state/{manifest,file,sync} | cluster token |
/api/services/* (CRUD + SDK) | service |
/api/services/{name}/console (WS) | admin |
/api/groups/* | service |
/api/status, /api/players, /api/broadcast, /api/maintenance/*, /api/proxy/* | service |
/api/controller/*, /api/commands/* | service |
/api/players/* (Players module) | service |
/api/permissions/*, /api/displays/*, /api/scaling/*, /api/punishments/* | service |
/api/resourcepacks/* (not /files) | service |
/api/reload, /api/shutdown, /api/config, /api/files/*, /api/doctor | admin |
/api/nodes, /api/loadbalancer, /api/modules/*, /api/audit | admin |
/api/dedicated/*, /api/modpacks/*, /api/plugins/*, /api/software/* | admin |
/api/stress/* | admin (rate-limited) |
/api/tokens, /api/tokens/scopes | admin |
/api/backups/* | admin |
For the streaming counterparts (live events, service consoles, Remote CLI multiplex), see WebSocket reference. For the event schema emitted over /api/events, see Events reference.