API reference
Every route Yggdrasil Panel serves, what it does, and what it takes to call it. The web UI talks to this API and nothing else, so anything the UI can do, you can automate.
Authentication
Yggdrasil accepts two kinds of credential, and one endpoint reads them in a fixed order:
Authorization: Bearer <token>— the header wins if present.- The
ygg_tokencookie. - The
?token=query parameter.
Session cookie
POST /api/auth/login with {"username", "password"} (plus "code" when the account has TOTP
enabled) returns a signed JWT and sets it as a ygg_token cookie. The cookie is HttpOnly,
SameSite=Strict, path /, and lives for the configured session TTL. The same JWT is also returned
in the response body.
Every request carrying a JWT is re-checked against the database: the panel reads the user’s current
role, refuses the request if the account is disabled, and refuses it if the token’s version no
longer matches the account’s token_version. That last check is what makes POST /api/auth/logout
a real revocation — it bumps token_version, invalidating every JWT ever issued to that user.
Because the role is read live, a demotion takes effect on the next request rather than at token
expiry.
API tokens
For automation, mint a token with POST /api/tokens and a body of {"name": "..."}. The response
contains the plaintext token exactly once:
{"id": "…", "name": "home-automation", "token": "ygg_…"}
Only a hash is stored, so a lost token cannot be recovered — delete it and mint another. Tokens
start with ygg_, which is how the auth middleware tells them apart from JWTs. Send one as
Authorization: Bearer ygg_…. GET /api/tokens lists your own tokens with their last-used
timestamp; DELETE /api/tokens/{id} removes one, and you may only delete your own.
Read Gotchas before you hand an API token to anything. A token is not a scoped credential.
WebSocket handshakes
Browsers cannot set headers on a WebSocket handshake, so the three streaming endpoints also accept
?token=<jwt-or-api-token> in the query string. The access log redacts token, access_token, and
api_key query parameters, so a token in a URL does not land in journald — but it does travel in
the request line, so prefer the cookie from a browser and the query parameter only where you have
no alternative.
Cross-origin requests and CSRF
The panel serves its own UI, so it never needs credentialed cross-origin access. CORS allows any
origin but never reflects credentials, and the allowed headers are Accept, Authorization,
Content-Type, and X-CSRF-Token.
On top of SameSite=Strict, the auth middleware rejects POST, PUT, PATCH, and DELETE
whenever the request carries an Origin header whose hostname differs from the request’s host. The
response is 403 {"error": "cross-origin request blocked"}. A browser on the panel’s own origin
matches and passes. Bearer automation — curl, a script, a home assistant — sends no Origin at all,
so the check does not apply to it. The WebSocket upgrader applies the same rule: an empty Origin
or a same-host one is accepted, anything else is refused.
Login rate limits
POST /api/auth/login is limited to 5 attempts per IP per minute; over that you get
429 {"error": "too many login attempts"}. The client IP comes from the real-IP middleware, so a
reverse proxy’s X-Forwarded-For is honoured.
Because a spoofed X-Forwarded-For would sidestep a purely IP-based limit, there is a second,
per-account lockout: 10 failed attempts on one username within 15 minutes locks that username for 15
minutes, regardless of source IP, with 429 {"error": "account temporarily locked due to repeated failed logins; try again later"}. A successful login clears the counter. A wrong password, an
unknown username, a bad TOTP code, and a replayed TOTP code all count as failures.
Permissions
A user is either a global admin or a delegate. Admins bypass every check in the panel — the
permission helpers return true for them before any grant is loaded, and requireAdmin routes
accept them unconditionally.
Everyone else holds grants. A grant is a set of permissions attached to one scope. There are eight permissions:
| Permission | Covers |
|---|---|
server.view |
Seeing the server, its status, stats, metrics, queries, and logs |
server.control |
Start, stop, restart, install, update, wipe, watchdog, config edits |
server.console |
The console stream, RCON, and player moderation |
server.files |
Browsing, reading, editing, uploading, downloading, deleting files |
server.create |
Creating servers within the scope |
server.delete |
Deleting servers |
server.backup |
Listing, running, verifying, restoring, and deleting backups |
server.schedule |
Creating schedules and reading their run history |
And four scopes a grant can hang off:
| Scope | Grant applies to |
|---|---|
global |
Every server |
realm |
Every server in that realm |
gameskill |
Every server built from that rune, in any realm |
server |
That one server |
A check passes when any single grant both contains the permission and covers the target. So a
server.control grant at realm scope lets you restart every server in that realm; a server.files
grant at gameskill scope lets you edit files on every DayZ server you can see, wherever it lives.
List endpoints filter rather than refuse: GET /api/servers returns only servers you can view, and
GET /api/domains and GET /api/schedules filter the same way.
Denied requests return 403 {"error": "forbidden: insufficient permissions"}. requireAdmin
returns 403 {"error": "forbidden"}.
Rune management, realm mutations, user management, and every integration setting are admin-only — a rune controls the Docker runtime (image, command, user, capabilities, devices, mounts), so uploading one is equivalent to root on the host.
Gotchas
Two behaviors here will not match what you assume from the shape of the API. Both are worth reading before you build against it.
API tokens carry no scope of their own
An ygg_ token is not a scoped credential. It is a pointer to a user. When the middleware sees the
ygg_ prefix it looks the token’s hash up in api_tokens, joins to the owning user, and builds
claims from that user’s id, username, and current role. There is no per-token permission subset and
no way to create one: a token minted by an admin is an admin token, and it passes every
requireAdmin route — user management, rune upload, system update, the lot. A token minted by a
delegate carries exactly that delegate’s grants, no more and no less. If you want a narrow
automation credential, create a user with narrow grants and mint the token as that user.
The token lookup filters on disabled=0, so disabling an account does immediately kill its tokens.
Logout does not revoke API tokens
The token_version mechanism that makes logout revoke JWTs does not reach API tokens. The claims
built for an ygg_ token omit the version field entirely, and the API-token branch of the
middleware returns before the code that compares a token’s version against the account’s. Bumping
token_version — which is all POST /api/auth/logout does — therefore has no effect on any ygg_
token. Logging out everywhere leaves every API token live.
To revoke an API token, delete it: DELETE /api/tokens/{id}. To revoke all of a user’s tokens at
once, disable or delete the user.
A delegate can create a schedule they cannot then touch
POST /api/schedules is permission-checked. For a server-scoped schedule it requires
server.schedule on that server, plus the permission the scheduled action would need if you ran it
by hand: server.console for a command, server.control for start/stop/restart/update/wipe,
server.backup for a backup. A rendered player message needs nothing beyond server.schedule. The
table is exhaustive and fails closed — an action with no entry is rejected. Realm- and
global-scoped schedules are admin-only.
The other three mutations are not permission-checked. PUT /api/schedules/{id},
DELETE /api/schedules/{id}, and POST /api/schedules/{id}/run are flat admin-only and return
403 {"error": "admin required"} for everyone else, with no consideration of grants.
So a delegate with server.schedule and server.control on a server can create a nightly restart,
see it in GET /api/schedules, and read its history at GET /api/schedules/{id}/runs — but cannot
edit its cron expression, cannot disable it, cannot delete it, and cannot trigger it early. Only an
admin can do those.
Conventions
Everything is JSON in and JSON out, with Content-Type: application/json.
Errors are a flat object with a single key, returned with the matching HTTP status:
{"error": "forbidden: insufficient permissions"}
JSON request bodies are read through a 1 MiB limit. The reader truncates at the cap rather than
returning a distinct error, so an oversized body surfaces as 400 {"error": "invalid request"} from
the decode. This applies to the pre-auth login endpoint too, which is the point: an unauthenticated
client cannot exhaust memory. A few endpoints have their own limits — rune upload caps the body at
512 KB, file upload is multipart with a 64 MiB form buffer, and the file editor refuses to open
anything over 5 MB.
There is deliberately no global request timeout. The console, log, and install streams are long-lived, and container operations like an image pull or a first start can run for minutes; a blanket timeout dropped both. Individual operations carry their own contexts instead — the AI planner, for instance, gives the model 60 seconds.
Responses set X-Content-Type-Options: nosniff, X-Frame-Options: DENY,
Referrer-Policy: strict-origin-when-cross-origin, and a strict CSP. HSTS is asserted only when the
request arrived over TLS or through a proxy that set X-Forwarded-Proto: https, so plain-HTTP LAN
access does not lock you out.
Worked example
Mint a token in the UI under Settings → API tokens, or over the API from an existing session. Then list your servers:
curl -s -H "Authorization: Bearer ygg_XXXXXXXX" \
https://panel.example.com/api/servers
Each entry carries the server’s id, name, rune, status, allocated ports, tags, and more. Abridged:
[
{
"id": "3f9c…",
"name": "survival",
"gameskill_id": "minecraft-java",
"status": "stopped",
"ports": {"game": 25565, "rcon": 25575},
"perms": ["server.view", "server.control", "server.console"]
}
]
The perms array on each server is the caller’s effective permissions on it — useful for deciding
what to attempt. An admin always gets all eight. Start one:
curl -s -X POST -H "Authorization: Bearer ygg_XXXXXXXX" \
https://panel.example.com/api/servers/3f9c…/start
No Origin header goes out, so the same-origin mutation check does not apply. Follow the console:
websocat "wss://panel.example.com/api/servers/3f9c…/console?token=ygg_XXXXXXXX"
Route reference
The Auth column names the strictest requirement. “Session” means any authenticated caller, JWT or API token. “Admin” means a global admin. A permission name means that permission on the target server, which an admin also satisfies.
Public
These need no credential at all. The status and beacon routes 404 rather than 403 when their feature is switched off, so a disabled status page or beacon receiver is not advertised.
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/login |
none | Exchange username/password (+ TOTP code) for a JWT and session cookie |
POST |
/api/auth/passkey/login/begin |
none | Start a WebAuthn login; returns the challenge |
POST |
/api/auth/passkey/login/finish |
none | Complete a WebAuthn login; issues the session |
GET |
/api/version |
none | Build version, repo URL, latest release, and whether an update exists |
GET |
/api/status |
none | Public status board JSON; 404 when the status page is off |
GET |
/status |
none | Public status page HTML; 404 when the status page is off |
GET |
/status.js |
none | The status page’s script, served same-origin for the CSP |
POST |
/api/beacon |
none | Receive an install ping; 404 unless this instance is the collector |
GET |
/api/beacon/count |
none | Installs seen in the last 30 days; 404 unless the collector opted into publishing. count is null below the threshold — “not saying”, not zero |
Session and account
| Method | Path | Auth | Description |
|---|---|---|---|
POST |
/api/auth/logout |
Session | Clear the cookie and bump token_version, revoking every JWT for the user |
GET |
/api/auth/me |
Session | The caller’s id, username, role, and the scopes they can create servers in |
GET |
/api/auth/2fa |
Session | Whether TOTP is enabled on the caller’s account |
POST |
/api/auth/2fa/setup |
Session | Generate a pending secret and return its otpauth:// URI |
POST |
/api/auth/2fa/enable |
Session | Verify a code against the pending secret and turn TOTP on |
POST |
/api/auth/2fa/disable |
Session | Turn TOTP off; requires a valid code |
GET |
/api/auth/passkey/credentials |
Session | List the caller’s registered passkeys |
POST |
/api/auth/passkey/register/begin |
Session | Start registering a passkey |
POST |
/api/auth/passkey/register/finish |
Session | Finish registering a passkey |
PUT |
/api/auth/passkey/credentials/{id} |
Session | Rename one of the caller’s passkeys |
DELETE |
/api/auth/passkey/credentials/{id} |
Session | Delete one of the caller’s passkeys |
GET |
/api/tokens |
Session | List the caller’s API tokens with last-used times |
POST |
/api/tokens |
Session | Mint an API token; the plaintext is returned once |
DELETE |
/api/tokens/{id} |
Session | Delete one of the caller’s own API tokens |
Runes
The API spells a rune gameskill. Reading the catalogue is open to any session because the
create-server form needs it; everything that changes the catalogue is admin-only.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/gameskills |
Session | List runes, each flagged with whether the caller can create servers from it |
GET |
/api/gameskills/{id} |
Session | The parsed rune definition |
POST |
/api/gameskills |
Admin | Upload a rune YAML (body capped at 512 KB) |
POST |
/api/gameskills/import-egg |
Admin | Convert a Pterodactyl egg JSON into a rune |
POST |
/api/gameskills/import-xml |
Admin | Import a rune expressed in XML |
GET |
/api/gameskills/github |
Admin | List runes in a GitHub repo directory. Each entry carries the repo copy’s version, plus installed / installed_version / builtin for the local one |
GET |
/api/gameskills/updates |
Admin | Installed non-builtin runes the catalog has moved past: {updates:[{id,name,installed_version,available_version,download_url}], checked_at, note?}. Matched by rune id against the community catalog; a note means the check couldn’t run, which is not the same as everything being current |
POST |
/api/gameskills/install-from-github |
Admin | Fetch, validate, and store one rune from GitHub |
DELETE |
/api/gameskills/{id} |
Admin | Delete a rune |
Servers
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/servers |
Session | List servers, filtered to those the caller can view |
POST |
/api/servers |
server.create |
Create a server in a realm from a rune |
GET |
/api/servers/{id} |
server.view |
One server, with the caller’s effective permissions |
PUT |
/api/servers/{id} |
server.control |
Edit name, variable values, resource caps, notes, notes_markdown. Changing realm_id or host_mounts requires admin — both are privilege-scoping fields |
DELETE |
/api/servers/{id} |
server.delete |
Delete the server |
POST |
/api/servers/{id}/clone |
server.view + server.create |
Copy a server’s setup into a fresh server with new ports and an empty data dir |
POST |
/api/servers/{id}/install |
server.control |
Run or re-run the install in the background |
POST |
/api/servers/{id}/start |
server.control |
Start the container |
POST |
/api/servers/{id}/stop |
server.control |
Stop the container |
POST |
/api/servers/{id}/restart |
server.control |
Restart the container |
POST |
/api/servers/{id}/safe-restart |
server.control |
Restart after warning players on a countdown |
GET |
/api/servers/{id}/auto-restart |
server.control |
The auto-restart toggle’s current state: {enabled, every_hours, anchor_hour, warn, backup_first, target_id} |
PUT |
/api/servers/{id}/auto-restart |
server.control |
Create, update, or remove the managed auto-restart schedule. every_hours 1–24, anchor_hour 0–23 is the hour the cycle starts from; backup_first requires target_id |
PUT |
/api/servers/{id}/watchdog |
server.control |
Toggle auto-heal for the server |
POST |
/api/servers/{id}/wipe |
server.control |
Delete the rune’s declared wipe paths, optionally backing up first |
GET |
/api/servers/{id}/stats |
server.view |
Live CPU and memory from Docker |
GET |
/api/servers/{id}/logs/export |
server.view |
Download the container log as text/plain. tail (a count or all), since/until (a duration like 2h, or RFC3339), timestamps=true. Streamed, not buffered. The log starts at the current container’s creation — a restart makes a new one, so there is no older history to ask for |
GET |
/api/servers/{id}/install/log/export |
server.view |
Download the buffered install log as text/plain. No range: it is the last 500 lines of the most recent install, held in memory and cleared by a panel restart |
GET |
/api/servers/{id}/metrics |
server.view |
Sampled history over the last N hours (default 24, max 168) |
GET |
/api/servers/{id}/quiet-hours |
server.view |
The calmest hour of day from 14 days of player samples |
GET |
/api/servers/{id}/query |
server.view |
Live game-protocol query: players, map, version |
GET |
/api/servers/{id}/battlemetrics |
server.view |
Online-status summary from BattleMetrics, when an id is configured |
GET |
/api/servers/{id}/reachability |
server.view |
Probe the public connect address to prove the port is forwarded |
GET |
/api/servers/{id}/admin-log |
server.view |
Parsed admin-log events, when the rune supports one |
GET |
/api/servers/{id}/delegates |
Admin | Every user holding a server-scoped grant on this server |
PUT |
/api/servers/{id}/delegates |
Admin | Replace the full set of server-scoped grants on this server |
Players and moderation
Player actions run over RCON, so they gate on server.console rather than server.control.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/servers/{id}/players |
server.console |
The online player list, or why it is unavailable |
POST |
/api/servers/{id}/players/kick |
server.console |
Kick a player |
POST |
/api/servers/{id}/players/broadcast |
server.console |
Broadcast a message in-game |
POST |
/api/servers/{id}/players/lock |
server.console |
Lock or unlock the server against new joins |
POST |
/api/servers/{id}/rcon |
server.console |
Send a raw RCON command |
GET |
/api/bans |
Admin | List centrally managed bans |
POST |
/api/bans |
Admin | Ban a player on one server or all of them |
DELETE |
/api/bans/{id} |
Admin | Lift a ban |
GET |
/api/violations |
Admin | List violation auto-action rules |
POST |
/api/violations |
Admin | Create a violation auto-action rule |
PUT |
/api/violations/{id} |
Admin | Edit a violation auto-action rule |
DELETE |
/api/violations/{id} |
Admin | Delete a violation auto-action rule |
Files
All of these resolve the server’s data directory and gate on server.files. Paths are jailed to
that directory, including through symlinks.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/servers/{id}/files |
server.files |
List a directory (?path=) |
GET |
/api/servers/{id}/files/content |
server.files |
Read a file for editing; refuses anything over 5 MB |
PUT |
/api/servers/{id}/files/content |
server.files |
Write a file, snapshotting the previous contents first |
GET |
/api/servers/{id}/files/versions |
server.files |
List snapshots for a path (metadata only) |
GET |
/api/servers/{id}/files/versions/{vid} |
server.files |
One snapshot’s full contents |
DELETE |
/api/servers/{id}/files |
server.files |
Delete a file or directory (?path=) |
POST |
/api/servers/{id}/files/upload |
server.files |
Multipart upload into a directory |
GET |
/api/servers/{id}/files/download |
server.files |
Download a single file |
The editor keeps 10 versions per file and only snapshots UTF-8 files up to 256 KB.
Backups
Backup targets are global configuration and admin-only. Individual backups gate on
server.backup for the server they belong to — for /api/backups/{id}/… the panel resolves the
backup’s server first, then checks.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/backup/targets |
Admin | List configured backup destinations |
POST |
/api/backup/targets |
Admin | Add a backup destination |
DELETE |
/api/backup/targets/{id} |
Admin | Remove a backup destination |
POST |
/api/backup/targets/{id}/test |
Admin | Verify a destination’s credentials and reachability |
GET |
/api/servers/{id}/backups |
server.backup |
List a server’s backups |
POST |
/api/servers/{id}/backup |
server.backup |
Run a backup now |
POST |
/api/backups/{id}/restore |
server.backup |
Restore a backup over the server’s data |
POST |
/api/backups/{id}/verify |
server.backup |
Check one backup’s integrity on demand |
DELETE |
/api/backups/{id} |
server.backup |
Delete a backup |
GET |
/api/settings/backup-verify |
Admin | The automatic backup-verification settings |
PUT |
/api/settings/backup-verify |
Admin | Update the automatic backup-verification settings |
GET |
/api/system/backup-coverage |
Admin | Installed servers with no recent successful backup (default window 7 days) |
Schedules and templates
Read Gotchas — the gates here are not symmetric.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/schedules |
Session | List schedules, filtered to those the caller has server.schedule on; realm/global rows are admin-only |
POST |
/api/schedules |
server.schedule + the action’s own permission |
Create a schedule; realm/global scope is admin-only |
PUT |
/api/schedules/{id} |
Admin | Edit a schedule, or toggle it on and off |
DELETE |
/api/schedules/{id} |
Admin | Delete a schedule |
POST |
/api/schedules/{id}/run |
Admin | Trigger a schedule immediately |
GET |
/api/schedules/{id}/runs |
server.schedule |
The last 50 executions; admin for realm/global schedules |
GET |
/api/templates |
Session | List message templates, built-in and custom |
POST |
/api/templates |
Admin | Create or update a message template |
DELETE |
/api/templates/{id} |
Admin | Delete a message template |
Managed schedule rows — the ones behind the per-server auto-restart toggle — are hidden from
GET /api/schedules and driven through /api/servers/{id}/auto-restart instead.
Users, realms, and permissions
Listing realms is open to any session because the create-server form needs it. Every other route here is admin-only: a realm is a permission scope, so renaming or deleting one moves servers between grant boundaries.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/realms |
Session | List realms. Admins also get server_count per realm; other callers don’t, since the list exists for the create-server form |
POST |
/api/realms |
Admin | Create a realm. name is required and unique — a clash is a 409, not a merge |
PUT |
/api/realms/{id} |
Admin | Rename or re-describe a realm. name is required here too: it writes both columns, so omitting it would blank the name servers are matched against |
DELETE |
/api/realms/{id} |
Admin | Delete a realm. Its servers are detached, not deleted; grants scoped to it stop applying |
GET |
/api/users |
Admin | List users |
POST |
/api/users |
Admin | Create a user |
PUT |
/api/users/{id} |
Admin | Update a user’s password, role, or disabled flag |
DELETE |
/api/users/{id} |
Admin | Delete a user |
GET |
/api/users/{id}/permissions |
Admin | A user’s grants |
PUT |
/api/users/{id}/permissions |
Admin | Replace a user’s grants |
GET |
/api/permissions/catalog |
Admin | The assignable permissions and scope types, for building an editor |
Kvasir
Kvasir’s features are opt-in and off until an admin configures a provider. Configuration is admin-only; the advisory endpoints gate on the permission that matches what they reveal.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/ai/config |
Admin | The AI provider configuration |
PUT |
/api/ai/config |
Admin | Update the AI provider configuration |
POST |
/api/ai/config/test |
Admin | Send a trivial prompt to verify credentials and endpoint |
POST |
/api/ai/health-digest |
Admin | An advisory cross-server ops briefing |
POST |
/api/servers/{id}/admin-log/digest |
server.view |
A plain-language read of recent admin-log activity |
POST |
/api/servers/{id}/explain |
server.view |
Explain an error from log text the caller is looking at |
POST |
/api/servers/{id}/config-advice |
server.control |
An advisory review of the server’s configuration |
POST |
/api/ai/plan |
Session | Turn a natural-language request into a previewable plan; executes nothing |
POST |
/api/ai/plan/execute |
Session | Run confirmed actions, re-checking each against RBAC |
/api/ai/plan and /api/ai/plan/execute need no explicit permission because they scope themselves:
both build the candidate set from the servers the caller holds server.control on, and the execute
step re-derives that set server-side rather than trusting the posted plan. Both refuse with 400
unless an admin has enabled AI actions.
Norn
Norn is the DayZ loot-economy tool. Reading the economy needs server.view; changing it needs
server.control, because these routes rewrite mission files.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/servers/{id}/dayz/economy |
server.view |
Types files, item and lifetime stats, cleanup timers |
GET |
/api/servers/{id}/dayz/mods |
server.view |
Each Workshop mod’s on-disk and upstream status, plus orphan folders |
GET |
/api/servers/{id}/dayz/mod-loot |
server.view |
Installed mods and the types.xml files each ships |
POST |
/api/servers/{id}/dayz/min-lifetime |
server.control |
Raise every item lifetime below a floor, in hours |
POST |
/api/servers/{id}/dayz/globals |
server.control |
Update allowlisted globals.xml cleanup timers |
POST |
/api/servers/{id}/dayz/register-types |
server.control |
Register detected types files in cfgeconomycore.xml |
POST |
/api/servers/{id}/dayz/import-mod-types |
server.control |
Copy a mod’s types.xml into the mission and register it |
POST |
/api/servers/{id}/dayz/reset |
server.control |
Clear saved Norn settings so reinstalls return to vanilla |
Networking and domains
The domain list is RBAC-filtered like the server list. Every integration setting is admin-only.
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/domains |
Session | Per-server subdomain and proxy state, filtered to viewable servers |
GET |
/api/domains/{id}/check |
server.view |
Probe one server’s public URL; the domain is recomputed server-side |
GET |
/api/settings/network |
Session | Public hostname, detected address, and the effective connect address |
PUT |
/api/settings/network |
Admin | Set the public hostname, UPnP toggle, and BattleMetrics token |
GET |
/api/upnp/status |
Admin | Whether UPnP is on and a gateway is reachable |
GET |
/api/settings/unifi |
Admin | The UniFi controller configuration |
PUT |
/api/settings/unifi |
Admin | Update the UniFi controller configuration |
POST |
/api/settings/unifi/test |
Admin | Log in and list rules to confirm it works |
GET |
/api/settings/npm |
Admin | The Nginx Proxy Manager configuration |
PUT |
/api/settings/npm |
Admin | Update the Nginx Proxy Manager configuration |
POST |
/api/settings/npm/test |
Admin | Log in and list proxy hosts to confirm it works |
GET |
/api/settings/cloudflare |
Admin | The Cloudflare tunnel configuration |
PUT |
/api/settings/cloudflare |
Admin | Update the Cloudflare tunnel configuration |
POST |
/api/settings/cloudflare/test |
Admin | Verify the token, resolve the zone, and check the tunnel config |
GET /api/settings/network is readable by any session because the connect address is what the UI
shows players; writing it is admin-only.
Steam
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/steam/account |
Admin | The authorized Steam account, if any |
POST |
/api/steam/send-code |
Admin | Attempt a login without a Guard code, prompting Steam to email one |
POST |
/api/steam/authorize |
Admin | Complete authorization with the Guard code |
DELETE |
/api/steam/account |
Admin | Remove the stored Steam credentials |
Status page, beacon, and Discord
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/settings/status-page |
Admin | The public status page configuration |
PUT |
/api/settings/status-page |
Admin | Update the public status page configuration |
GET |
/api/settings/beacon |
Admin | The beacon configuration |
PUT |
/api/settings/beacon |
Admin | Update the beacon configuration |
POST |
/api/settings/beacon/test |
Admin | Send a one-off ping to confirm a collector is reachable |
GET |
/api/beacon/stats |
Admin | Collected install counts, when this instance is a collector |
GET |
/api/settings/discord-status |
Admin | The Discord status board configuration |
PUT |
/api/settings/discord-status |
Admin | Update the Discord status board configuration |
POST |
/api/settings/discord-status/post |
Admin | Force an immediate status board refresh |
The public status board caches for 15 seconds and is served with Cache-Control: public, max-age=15.
Notifications
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/notifications |
Admin | List notification channels |
POST |
/api/notifications |
Admin | Add a notification channel |
DELETE |
/api/notifications/{id} |
Admin | Remove a notification channel |
POST |
/api/notifications/{id}/test |
Admin | Send a test notification |
Audit
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/audit |
Admin | The audit log, filterable |
GET |
/api/audit/export |
Admin | The same filtered log as a CSV download |
System
| Method | Path | Auth | Description |
|---|---|---|---|
GET |
/api/system/info |
Admin | Docker health, object counts, and host CPU/memory/disk figures |
GET |
/api/system/os-updates |
Admin | Pending host OS updates: {supported, total, security?, reboot_required, reboot_pkgs?, cache_age_hours?, source}. Read-only — the panel reports, it never applies. security is absent when the host can’t tell, which is not the same as zero. Cached 15 minutes |
POST |
/api/system/update |
Admin | Update the panel to the latest official release |
POST |
/api/system/check-update |
Admin | Force a fresh release check, bypassing the 6-hour cache |
GET |
/api/system/update-status |
Admin | The last update result written by the helper, including failures |
GET |
/api/system/auto-update |
Admin | The opt-in scheduled updater’s settings |
POST |
/api/system/auto-update |
Admin | Update the scheduled updater’s settings |
Anything not matching /api/ falls through to the embedded SPA, which serves index.html for
unknown paths so client-side routes deep-link correctly. Unknown /api/ paths return 404.
WebSocket endpoints
Three endpoints upgrade to WebSocket. All three authenticate exactly like the rest of the API — the
ygg_token cookie is enough from a browser, and ?token= carries a JWT or an ygg_ API token from
anything that cannot set headers. The upgrader accepts a handshake with no Origin (automation) or
one whose hostname matches the request host, and refuses everything else.
| Path | Auth | Streams |
|---|---|---|
/api/servers/{id}/install/log |
server.view |
Install and build output — buffered history, then live |
/api/servers/{id}/logs |
server.view |
Container logs, live |
/api/servers/{id}/console |
server.console |
Container output, and input sent over RCON or the container’s stdin |
/console is the one that writes, which is why it gates on server.console while /logs needs only
server.view. Each message you send it is one command: it goes over RCON when the server’s rune
declares an enabled rcon block, and to the container’s stdin otherwise. Start an install with POST /api/servers/{id}/install and follow /install/log for
progress; the install itself runs in the background and does not block the POST.