Base URL: your panel origin (e.g. https://panel.example.com).
All /api/* routes except POST /api/auth/login require authentication via:
- Session cookie —
panel_session, set by login; or - API key —
Authorization: Bearer maryl_<token>
Unauthenticated requests receive 401 with { "error": "Unauthorized" }.
| Status | Meaning |
|---|---|
400 |
Invalid input — { "error": "message" } |
401 |
Not authenticated |
403 |
Authenticated but not permitted |
404 |
Resource not found — { "error": "message" } |
202 |
Accepted — async job started |
201 |
Created |
429 |
Login rate limited — { "error": "Too many login attempts…" } with Retry-After header |
JSON request bodies use Content-Type: application/json unless noted.
Three layers apply, in order:
- Authentication — valid session or API key
- Global role —
ADMIN,OPERATOR, orVIEWER - Server permission — for routes under
/api/servers/{slug}/…
| Helper | Allows |
|---|---|
| Any authenticated user | VIEWER, OPERATOR, ADMIN |
| Write | OPERATOR, ADMIN |
| Admin | ADMIN only |
Used by requireServerPermission(slug, permission):
| Permission | Typical routes |
|---|---|
read |
Server detail, metrics, backup list, schedules list |
power |
Lifecycle, schedule create/update/delete/run |
console |
Logs, log stream, RCON |
files.read |
File list/read, config GET, SFTP info |
files.write |
File write/upload, config PUT |
backup |
Create backup, download archive |
admin |
Members, backup restore/delete, migrate |
delete |
Delete server |
Global shortcuts (no ServerMember row needed):
ADMIN→ all permissions on all serversOPERATOR→ all exceptdeleteandadminVIEWER→readonly; needs membership for anything else
POST /api/jobs/{id}/run and POST /api/jobs/{id}/cancel require write access and permission derived from the job:
| Job type | Required permission |
|---|---|
PROVISION, RESTORE, MIGRATE |
admin on job's server |
START, STOP, RESTART, UPDATE |
power |
BACKUP |
backup |
DELETE |
delete |
FILE_OPERATION |
files.write |
DATABASE_BACKUP, DATABASE_RESTORE |
global ADMIN (no server) |
Public. Rate limited (20 attempts / 10 min per IP, 8 per email).
Body:
{ "email": "admin@panel.local", "password": "admin" }Response 200:
{ "ok": true }Sets panel_session cookie.
Destroys the current session.
Response 200:
{ "ok": true }Auth: any user.
Returns fleet summary — server counts by status, node capacity, recent alerts, batch CPU/RAM metrics.
Auth: any user.
Compact server list for navigation and dropdowns.
Auth: any user.
Query: ?limit=50 (optional).
Audit log entries (user actions across the panel).
Auth: any user.
All registered nodes with capacity usage and server counts.
Auth: admin.
Body:
{
"name": "node-1",
"hostname": "10.0.0.5",
"dockerHost": "unix:///var/run/docker.sock",
"dockerAuthToken": null,
"cpuTotal": 16,
"memoryTotalMb": 65536,
"diskTotalGb": 500
}name must match ^[a-z0-9-]+$.
Auth: any user. Docker auth token is not returned (only hasDockerAuthToken: true/false).
Auth: admin.
Body (all optional):
{
"hostname": "10.0.0.5",
"dockerHost": "unix:///var/run/docker.sock",
"dockerAuthToken": "…",
"status": "ONLINE",
"drainMode": false,
"maintenanceMode": false,
"cpuTotal": 16,
"memoryTotalMb": 65536,
"diskTotalGb": 500,
"portRanges": [{ "start": 27015, "end": 27100, "protocol": "udp" }]
}Auth: admin. Manually triggers Docker ping, capacity sync, and disk usage scan.
Auth: any user.
Auth: admin. Create a custom game template.
Auth: any user.
Auth: admin.
Auth: any user.
Query: ?node={name} for per-node port detail.
Auth: any user.
Auth: any user. Supports ?status= filter (lists alerts, not a single-resource fetch).
Auth: write.
Body:
{ "action": "acknowledge" }or { "action": "resolve" }.
Auth: admin. Creates server record and enqueues PROVISION job.
Body:
{
"name": "my-cs2-server",
"templateSlug": "cs2",
"nodeName": "node-1",
"cpuLimit": 4,
"memoryLimitMb": 8192,
"diskLimitGb": 40,
"ports": [
{ "name": "game", "protocol": "udp", "internalPort": 27015, "externalPort": 27015 },
{ "name": "query", "protocol": "udp", "internalPort": 27016, "externalPort": 27016 },
{ "name": "rcon", "protocol": "tcp", "internalPort": 27020, "externalPort": 27020 }
],
"envVars": [
{ "key": "LGSM_GAMESERVER", "value": "cs2server" }
]
}Response 201: { "server": {…}, "job": {…} }
Auth: server read.
Server detail with live Docker stats when available.
Auth: server delete. Enqueues DELETE job.
Auth: server power.
Body:
{ "action": "start" }action: start | stop | restart.
Response 202: { "job": {…} }
Auth: server admin.
Body:
{ "targetNodeId": "clxyz…" }Auth: server read.
Historical CPU and memory samples.
Auth: server console.
Query: ?tail=300, ?since=<unix_ms>.
Snapshot of container logs (JSON).
Auth: server console.
Server-Sent Events stream of live container logs.
Auth: server console + global write (OPERATOR+).
Body:
{ "mode": "shell" }Response:
{
"sessionId": "<64 hex>",
"mode": "shell",
"wsPath": "/api/console/ws",
"expiresAt": "2026-01-01T00:00:00.000Z"
}Connect WebSocket to ws://{host}:{CONSOLE_WS_PORT}/api/console/ws?session={sessionId} with the panel_session cookie present. Session is single-use.
Auth: server console. Server must be RUNNING.
Body:
{ "command": "status" }Response:
{
"command": "status",
"output": "…",
"timestamp": "2026-01-01T00:00:00.000Z"
}Auth: server files.read.
| Query | Behavior |
|---|---|
?path= |
List directory (default root) |
?path=…&mode=file |
Read text file content |
Auth: server files.write.
Body:
{ "path": "serverfiles/csgo/cfg/server.cfg", "content": "…" }Auth: server files.write.
Body (one of):
{ "action": "mkdir", "path": "newdir" }
{ "action": "delete", "path": "oldfile.txt" }
{ "action": "rename", "path": "old", "newPath": "new" }Auth: server files.write. multipart/form-data with file and path fields. Max size: FILE_UPLOAD_MAX_BYTES.
Auth: server files.read.
Structured game config view — field definitions from the template profile with current values. Secret fields return ******** instead of the real value.
Auth: server files.write.
Body:
{
"fields": {
"hostname": "My Server",
"rcon_password": "********"
}
}Send ******** to keep an existing secret unchanged.
Auth: server files.read.
Returns SSH/SFTP connection hints (host, port, username, data path). Does not provision credentials — uses your host's SSH.
Auth: server read.
Auth: server backup. Creates tarball asynchronously.
Response 202: { "backup": {…} }
Auth: server read. Does not include storagePath.
Auth: server admin.
Auth: server backup. Returns the archive file stream.
Auth: server admin.
Body:
{ "confirm": true }Stops the server, optionally creates a safety backup, extracts the archive, restarts.
Auth: server read.
Auth: server power. Creator must hold every permission required by the schedule's tasks.
Body:
{
"label": "Nightly backup",
"cron": "0 4 * * *",
"timezone": "UTC",
"enabled": true,
"tasks": [
{ "action": "BACKUP" },
{ "action": "RESTART" }
]
}Task actions: BACKUP, START, STOP, RESTART, CONSOLE_COMMAND.
CONSOLE_COMMAND is accepted by the API but not executed by the schedule runner.
Auth: server power.
Body:
{ "enabled": false, "label": "Updated label" }Auth: server power.
Auth: server power. Run all tasks immediately.
Auth: server admin.
Auth: server admin.
Body:
{
"userId": "clxyz…",
"permissions": {
"power": true,
"console": true,
"files.read": true,
"files.write": false,
"backup": true,
"delete": false,
"admin": false
}
}Omit permissions to use defaults based on the user's global role.
Auth: server admin. Update permissions object.
Auth: server admin.
Auth: any user. Templates and nodes with placement/capacity checks.
Auth: any user.
Body:
{
"nodeId": "clxyz…",
"ports": [
{ "externalPort": 27015, "protocol": "udp" }
]
}Response:
{ "valid": true, "conflicts": [] }Auth: any user. Suggest an available port.
Body:
{ "nodeId": "clxyz…", "protocol": "udp", "preferred": 27015 }Auth: any user.
Query: ?status=PENDING, ?server={slug}.
Auth: write + job permission (see table above). Retries failed or stuck jobs.
Auth: write + job permission.
Auth: admin.
Auth: admin.
Body:
{
"email": "ops@example.com",
"name": "Operator",
"password": "minimum-8-chars",
"role": "OPERATOR"
}role: ADMIN | OPERATOR | VIEWER (default OPERATOR).
Auth: admin.
Body (all optional):
{ "name": "…", "role": "OPERATOR", "password": "new-password" }Password change revokes all sessions and API keys for that user.
Auth: admin. Cannot delete yourself or the last admin.
Auth: admin.
Auth: admin.
Body:
{ "name": "terraform" }Response 201:
{
"key": {
"id": "…",
"name": "terraform",
"token": "maryl_a1b2c3…",
"createdAt": "…"
}
}token is shown once.
Auth: admin. Revokes the key.
Auth: admin.
Auth: admin. Enqueues DATABASE_BACKUP job (pg_dump).
Auth: admin. Download dump file.
Auth: admin. Enqueues DATABASE_RESTORE job. Destructive — replaces the panel database.
Auth: admin. Current recovery/safety-backup state.
Auth: any authenticated user.
Query: ?width=120 (terminal width for ASCII art wrapping).
Auth: admin. Parsed maryl.yaml as JSON.
Auth: admin. Partial branding update.
Auth: admin. Reset to defaults.
Auth: admin. Raw YAML text.
Auth: admin.
Body:
{ "raw": "branding:\n enabled: true\n…" }Invalid YAML returns 400.
# Login (save cookie)
curl -c cookies.txt -X POST https://panel.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@panel.local","password":"admin"}'
# Or use an API key
export MARYL_TOKEN="maryl_…"
# Create server
curl -b cookies.txt -X POST https://panel.example.com/api/servers \
-H "Content-Type: application/json" \
-d '{
"name": "arena-1",
"templateSlug": "cs2",
"nodeName": "node-1",
"cpuLimit": 4,
"memoryLimitMb": 8192,
"diskLimitGb": 40,
"ports": [
{"name":"game","protocol":"udp","internalPort":27015,"externalPort":27015},
{"name":"query","protocol":"udp","internalPort":27016,"externalPort":27016},
{"name":"rcon","protocol":"tcp","internalPort":27020,"externalPort":27020}
],
"envVars": [{"key":"LGSM_GAMESERVER","value":"cs2server"}]
}'
# Poll job status
curl -b cookies.txt "https://panel.example.com/api/jobs?server=arena-1"