Nimbusv1.0.0

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 URLhttp://{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:

LevelCredentialTypical caller
Publicnone/api/health, /api/resourcepacks/files/{uuid}.zip
Cluster token[cluster] token (separate secret)Agents — for bootstrap, template download, state sync
Service tokenHMAC-SHA256 derivation of the master API tokenBackend SDK, Velocity Bridge
Bearer (master)[api] token literal OR JWT with the admin scopeDashboard, Remote CLI, operators
Admin onlymaster token / JWT admin only — derived service tokens are rejectedAll 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
admin

admin 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_origins are accepted for GET/POST/PUT/DELETE/PATCH with both http and https schemes. An empty list defaults to "any origin" (useful for dev, loud warning in logs for prod).
  • Rate limits — global 120 req/min per client IP; /api/stress/start|stop|ramp falls under a stricter 5 req/min bucket. The client IP comes from X-Forwarded-For when [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/infoservice

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/changelogservice

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/statusservice

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/playersservice

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}/sendservice

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}/kickservice

Kicks a player from the network. Executed via Velocity's velocity kick command.

Body{ "reason": "AFK" }

POST /api/broadcastservice

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

MethodPathNotes
POST/api/services/{groupName}/startStarts 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}/stopGraceful stop.
POST/api/services/{name}/restartStop + start.
POST/api/services/{name}/migrateMove to another node. Body { "target": "worker-1" }, omit/null to let the placement strategy choose.
POST/api/services/{name}/execBody { "command": "say hi" }. Writes to the process stdin.

SDK endpoints

Backends (using :nimbus-sdk) call these:

MethodPathBody
PUT/api/services/{name}/state{ "customState": "waiting" } — requires service to be READY.
GET/api/services/{name}/stateCurrent 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=100Tails 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

MethodPathAuthNotes
GET/api/groupsserviceList with activeInstances and scanned modIds.
GET/api/groups/{name}serviceSingle group.
POST/api/groupsserviceCreate. Writes config/groups/{name}.toml + reloads.
PUT/api/groups/{name}serviceReplace. Immutable name.
DELETE/api/groups/{name}serviceDeletes 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>/.

MethodPathNotes
GET/api/dedicatedList with runtime status.
GET/api/dedicated/{name}Detail.
POST/api/dedicatedCreate — 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/importImport a CurseForge or Modrinth modpack into the dedicated dir.
POST/api/dedicated/{name}/modpack/uploadUpload 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.

MethodPathBody / notes
GET/api/maintenanceFull 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).

MethodPathNotes
GET/api/proxy/configUnified read used by the Bridge on connect. Includes maintenance snapshot.
GET / PUT/api/proxy/tablistTablist header/footer/playerFormat/updateInterval.
GET / PUT/api/proxy/motdMOTD lines + maxPlayers + offset.
GET / PUT/api/proxy/chatChat format + enabled.
GET/api/proxy/tablist/playersAll UUID overrides.
PUT/api/proxy/tablist/players/{uuid}{ "format": "&7[Afk] {name}" }
DELETE/api/proxy/tablist/players/{uuid}Clear override.

POST /api/proxy/eventsservice

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.

MethodPathNotes
GET/api/commandsEvery 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).

MethodPathNotes
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

MethodPathNotes
POST/api/reloadHot-reloads every config/groups/*.toml. Emits CONFIG_RELOADED. Returns a ReloadReport (v0.12.0+).
POST/api/shutdownResponds 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.

MethodPathPurpose
GET/api/services/{name}/state/manifestCurrent StateManifest JSON.
GET/api/services/{name}/state/file/{path...}Single file raw bytes.
POST/api/services/{name}/state/syncMultipart 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

MethodPathNotes
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

MethodPathNotes
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/scopesSorted list of every scope the controller accepts.

Modules — admin

MethodPathNotes
GET/api/modulesEvery 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

MethodPathNotes
GET/api/softwareAll ServerSoftware enum values with capability flags.
GET/api/software/{type}/versionsStable + 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

MethodPathNotes
POST/api/modpacks/resolveLook up a pack by URL or ID.
POST/api/modpacks/importImport by manifest (CurseForge/Modrinth).
POST/api/modpacks/uploadSingle-shot upload of a server pack (raw body, filename query param).
POST/api/modpacks/upload/initStart a chunked upload — returns an upload ID.
POST/api/modpacks/upload/chunkAppend a chunk.
POST/api/modpacks/upload/finalizeCommit 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

MethodPathAuthNotes
GET/api/stressadminStatus — subject to the global 120/min limit.
POST/api/stress/startadminBody { "players": 100, "group": "Lobby", "rampSeconds": 30 }. Rate limited 5/min.
POST/api/stress/stopadmin5/min.
POST/api/stress/rampadmin{ "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:

MetricTypeLabelsDescription
nimbus_warmpool_sizegaugegroupCurrently prepared services in the warm pool for this group.
nimbus_warmpool_targetgaugegroupConfigured scaling.warm_pool_size target.
nimbus_service_crashes_totalcountergroupService crashes since controller start. Group label is inferred from the <Group>-<N> naming convention; dedicated services tally under their own name.
nimbus_scaling_events_totalcountergroup, direction=up|downScaling decisions emitted by the engine.
nimbus_placement_blocked_totalcountergroupStarts 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.

MethodPathNotes
POST/api/console/completeTab completion — { "buffer": "ser", "cursor": 3 }. Returns candidate list.
WS/api/console/streamMultiplexed 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

MethodPathNotes
GET/api/players/onlineCanonical online roster from the Bridge-fed tracker.
GET/api/players/online/{uuid}Single live session.
GET/api/players/history/{uuid}?limit=20Session history rows.
GET/api/players/info/{uuid}Player meta + online flag + current service.
GET/api/players/all?q=&limit=50Search 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:

MethodPathPurpose
GET / POST/groupsList / create
GET / PUT / DELETE/groups/{name}Read / update / delete
POST / DELETE/groups/{name}/permissionsAdd / remove perms (context server, world, expiresAt)
GET / PUT/groups/{name}/metaGroup meta (read list, set key)
DELETE/groups/{name}/meta/{key}Remove a meta key
GET/players?q=&limit=50Search 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}/groupsAdd / remove a group (context in body)
GET / PUT/players/{uuid}/metaPlayer 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/tracksList tracks.
GET / DELETE/tracks/{name}Read / delete a track.
POST/tracksCreate 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/permissionsAdd one permission to many groups.
POST/bulk/groupsAssign one group to many players.
GET/audit?limit=50&offset=0Perms-specific audit log.

Display module — service

MethodPathNotes
GET/api/displaysAll 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}/resetReset 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/*:

MethodPathNotes
GET/statusGroups with active schedule rule + 10 most recent decisions.
GET/schedulesEvery group's schedule config (rules, warmup).
GET/schedules/{group}Per-group schedule.
GET/history/{group}?hours=24Player count snapshots.
GET/predictions/{group}Next 6 hours, predicted players + sample count.
GET/decisions?limit=50Recent ScalingDecisions rows.

Punishments module — service

MethodPathNotes
GET/api/punishments?active=true&type=&limit=&offset=List.
POST/api/punishmentsIssue — 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=100Full 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/messagesRead / 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/*

MethodPathNotes
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}/assignmentsBody { "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/statusBackend 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.

MethodPathNotes
GET / PUT/api/backups/configLive 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/schedulesScheduler view of cron entries.
GET/api/backups/status{ "activeJobs", "localDestination", "schedules" }.
GET/api/backups/{id}Record.
GET/api/backups/{id}/manifestStreams the trailing MANIFEST.sha256 entry.
GET/api/backups/{id}/downloadRaw tar+zstd archive (application/zstd).
POST/api/backups/triggerBody { "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}/verifyRe-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.

AuthAUTH_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

GenericVALIDATION_FAILED, INTERNAL_ERROR, PAYLOAD_TOO_LARGE, NO_FIELDS_TO_UPDATE

ServiceSERVICE_NOT_FOUND, SERVICE_NOT_READY, SERVICE_UNAVAILABLE, SERVICE_START_FAILED, SERVICE_STOP_FAILED, SERVICE_RESTART_FAILED

GroupGROUP_NOT_FOUND, GROUP_ALREADY_EXISTS, GROUP_HAS_RUNNING_INSTANCES

DedicatedDEDICATED_NOT_FOUND, DEDICATED_ALREADY_EXISTS, DEDICATED_ALREADY_RUNNING, DEDICATED_DIRECTORY_NOT_FOUND, DEDICATED_PORT_IN_USE

CommandCOMMAND_NOT_FOUND, COMMAND_NOT_REMOTE, COMMAND_EXECUTION_FAILED

StressSTRESS_ALREADY_RUNNING, STRESS_NOT_RUNNING

Cluster / LBCLUSTER_NOT_ENABLED, CLUSTER_TOKEN_MISSING, LOAD_BALANCER_NOT_ENABLED, NODE_NOT_FOUND

FilesINVALID_SCOPE, PATH_NOT_FOUND, PATH_TRAVERSAL

ProxyPROXY_NOT_AVAILABLE

TemplateTEMPLATE_NOT_FOUND

ModpackMODPACK_NOT_FOUND, MODPACK_INVALID, MODPACK_UPLOAD_FAILED, CHUNKED_UPLOAD_NOT_FOUND, CHUNKED_UPLOAD_INVALID, CURSEFORGE_API_KEY_MISSING

PluginPLUGIN_VERSION_NOT_FOUND

SoftwareSOFTWARE_UNKNOWN

ScalingSCALING_CONFIG_NOT_FOUND

PlayersPLAYER_NOT_FOUND, PLAYER_NOT_ONLINE

PermsPERMISSION_GROUP_NOT_FOUND, PERMISSION_TRACK_NOT_FOUND

DisplayDISPLAY_CONFIG_NOT_FOUND

PunishmentsPUNISHMENT_NOT_FOUND, PUNISHMENT_ALREADY_REVOKED, PUNISHMENT_TARGET_INVALID, PUNISHMENT_DURATION_INVALID

Resource packsRESOURCE_PACK_NOT_FOUND, RESOURCE_PACK_ALREADY_EXISTS, RESOURCE_PACK_INVALID_URL, RESOURCE_PACK_UPLOAD_FAILED, RESOURCE_PACK_ASSIGNMENT_NOT_FOUND

BackupBACKUP_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 prefixAuth
/api/healthpublic
/api/metricsservice
/api/resourcepacks/files/{uuid}.zippublic (hash-signed)
/api/cluster/bootstrapcluster token
/api/templates/{name}/download, /hashcluster 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/doctoradmin
/api/nodes, /api/loadbalancer, /api/modules/*, /api/auditadmin
/api/dedicated/*, /api/modpacks/*, /api/plugins/*, /api/software/*admin
/api/stress/*admin (rate-limited)
/api/tokens, /api/tokens/scopesadmin
/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.

On this page