Documentation menu

REST API Reference

Authentication

There are two ways to authenticate against /api/*. Browser sessions and the dashboard rely on the cookie + CSRF flow; the desktop app, scripted clients, and custom integrations use the Bearer header.

POST /api/auth/login sets two cookies:

  • auth-token — HttpOnly, SameSite=Lax, Secure when served over HTTPS. The middleware reads this first and treats it as a logged-in session. JavaScript can't see it.
  • csrf-token — readable by JavaScript. Echo its value back as the X-CSRF-Token header on every state-changing request (POST/PUT/PATCH/DELETE). The backend signs the token and rejects mismatches with 403 csrf-mismatch.

Browser clients should send fetch requests with credentials: 'include':

const res = await fetch('/api/servers/123/restart', {
  method: 'POST',
  credentials: 'include',
  headers: { 'X-CSRF-Token': getCookie('csrf-token') },
});

Paths exempt from CSRF (no X-CSRF-Token required):

  • POST /api/auth/login, POST /api/auth/logout
  • All /api/setup/*
  • GET /api/health
  • POST /api/store/webhook (signed by Paddle)
  • POST /api/discord/action (signed by the Discord bot — see Discord bot doc)

Bearer token (desktop / scripted clients)

The same POST /api/auth/login returns a JWT in the response body. Custom or scripted clients pass it as a Bearer header:

Authorization: Bearer <jwt-token>

The middleware reads cookie first and falls back to Bearer, so a single login can drive either flow. Bearer is the right choice when credentials: 'include' isn't an option (CLIs, native apps, server-to-server proxies).

Endpoints

MethodPathAuthDescription
POST/api/auth/loginNoneAuthenticate with username/password. Sets auth-token + csrf-token cookies AND returns JWT in body. Per-(IP, username) brute-force lockout: 5 attempts, escalating 60s → 5min → 1h.
POST/api/auth/logoutAny authClears the auth-token cookie and revokes the JWT. Exempt from CSRF.
POST/api/auth/mfa/setupRequiredSetup MFA for account
POST/api/auth/mfa/verifyRequiredVerify MFA code
POST/api/auth/mfa/disableRequiredDisable MFA
POST/api/auth/change-password-forcedRequiredForce password change

Request (login):

{
  "username": "admin",
  "password": "your-password"
}

Response (login):

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "user": { "id": "1", "username": "admin", "role": "admin" }
}

The Set-Cookie headers on the same response carry auth-token and csrf-token for browser flows.


Servers

MethodPathPermissionDescription
GET/api/serversAny authList all servers with live status, player count, CPU/RAM
POST/api/serversserver.deployCreate a new server entry (name, installDir required; validates DayZ binary exists)
POST/api/servers/detectserver.deployDetect existing DayZ server in directory
PATCH/api/servers/:idserver.deployUpdate server properties
DELETE/api/servers/:idserver.deployDelete a server entry (must be stopped)
POST/api/servers/batchserver.deployBatch start/stop/restart

Server Control

MethodPathPermissionDescription
GET/api/servers/:id/statusAny authFull server status: players, uptime, CPU/RAM, map, version
POST/api/servers/:id/startserver.startStart the DayZ server process
POST/api/servers/:id/stopserver.stopGraceful stop (RCON shutdown → kill process)
POST/api/servers/:id/restartserver.restartRestart with up to 3 retries. Fires webhooks
POST/api/servers/:id/lockserver.rconLock server (prevent new joins)
POST/api/servers/:id/unlockserver.rconUnlock server
POST/api/servers/:id/updateserver.deployTrigger manual update
GET/api/servers/:id/update/statusAny authGet update state
POST/api/servers/:id/update/cancelserver.deployCancel update

RCON & Players

MethodPathPermissionDescription
POST/api/servers/:id/rconserver.rconSend raw RCON command. Body: { "command": "..." }
POST/api/servers/:id/messagechat.sendBroadcast global message. Body: { "message": "..." }
GET/api/servers/:id/playersplayers.viewList online players
POST/api/servers/:id/players/:playerId/kickplayers.kickKick player. Body: { "reason": "..." }
POST/api/servers/:id/players/:playerId/banplayers.banBan player via global ban database. Body: { "reason": "...", "expiration": "..." }

Admin Actions

These actions route through the Provider System (InHouseProvider → Sidecar, or RCON fallback).

MethodPathPermissionDescription
GET/api/servers/:id/actions/capabilitiesserver.viewGet available actions
POST/api/servers/:id/actions/healserver.rconHeal a player. Body: { "steamId": "..." }
POST/api/servers/:id/actions/killserver.rconKill player
POST/api/servers/:id/actions/teleportserver.rconTeleport player. Body: { "steamId", "x", "y", "z" }
POST/api/servers/:id/actions/spawn-itemserver.rconSpawn item. Body: { "steamId", "itemClass", "quantity" }
POST/api/servers/:id/actions/unstuckserver.rconUnstuck player
POST/api/servers/:id/actions/freezeserver.rconFreeze/unfreeze player
POST/api/servers/:id/actions/stripserver.rconStrip inventory
POST/api/servers/:id/actions/explodeserver.rconExplode player
POST/api/servers/:id/actions/messageserver.rconSend message to player
GET/api/servers/:id/actions/player/:steamIdplayers.viewPlayer details

Vehicle Actions

MethodPathBodyDescription
POST/api/servers/:id/actions/vehicle/delete{ "vehicleId" }Delete a vehicle
POST/api/servers/:id/actions/vehicle/repair{ "vehicleId" }Repair a vehicle
POST/api/servers/:id/actions/vehicle/refuel{ "vehicleId" }Refuel a vehicle
POST/api/servers/:id/actions/vehicle/unstuck{ "vehicleId" }Unstuck a vehicle
POST/api/servers/:id/actions/vehicle/explode{ "vehicleId" }Explode a vehicle

World Actions

MethodPathBodyDescription
POST/api/servers/:id/actions/world/time{ "hour", "minute" }Set world time
POST/api/servers/:id/actions/world/weather{ "overcast", "rain", "fog", "snow", "wind" }Set weather (values 0.0–1.0)
POST/api/servers/:id/actions/world/sunnyClear weather
POST/api/servers/:id/actions/world/wipe-aiRemove all AI entities
POST/api/servers/:id/actions/world/wipe-vehiclesRemove all vehicles

Global Ban Database

Centralized ban system with UUID-based shareable ban IDs. Bans apply to all servers and are automatically synced to each server's ban.txt on start/restart.

MethodPathPermissionDescription
GET/api/bansbans.manageList all bans in the global database
POST/api/bansbans.manageAdd a manual ban. Body: { "steamId", "playerName", "reason", "expiresAt" }
DELETE/api/bans/:idbans.manageRemove a ban by UUID. Cleans all server ban.txt files
GET/api/bans/:idbans.manageGet a single ban by UUID
GET/api/bans/exportbans.manageExport all bans as a downloadable JSON file
POST/api/bans/importbans.manageImport bans from a JSON array. Returns { added, skipped, errors, total }

Import format (POST body is a JSON array):

[
  {
    "steamId": "76561198012345678",
    "playerName": "PlayerName",
    "reason": "Cheating",
    "bannedBy": "admin",
    "bannedAt": "2026-03-05T12:00:00.000Z"
  }
]

Export response (download as citadel-bans-YYYY-MM-DD.json):

[
  {
    "id": "a1b2c3d4-...",
    "steamId": "76561198012345678",
    "playerName": "PlayerName",
    "reason": "Cheating",
    "bannedBy": "admin",
    "bannedAt": "2026-03-05T12:00:00.000Z",
    "source": "manual"
  }
]

Mods

MethodPathPermissionDescription
GET/api/servers/:id/modsmods.viewList installed mods
POST/api/servers/:id/mods/installmods.installInstall mod (workshopId). Body: { "workshopId", "name" }
DELETE/api/servers/:id/mods/uninstall/:workshopIdmods.installUninstall mod
PATCH/api/servers/:id/mods/:workshopIdmods.installUpdate mod properties (enabled, load order)
PATCH/api/servers/:id/mods/:modName/typemods.installSet mod type (client/server)
POST/api/servers/:id/mods/reordermods.installReorder load priority
POST/api/servers/:id/mods/check-updatesmods.installCheck for updates
POST/api/servers/:id/mods/update/:workshopIdmods.installUpdate single mod
POST/api/servers/:id/mods/update-allmods.installUpdate all mods
GET/api/servers/:id/mods/updatesmods.viewGet pending updates
DELETE/api/servers/:id/mods/updates/:workshopIdmods.installClear pending update
GET/api/mods/install-statusmods.viewGet install progress
GET/api/mods/cache/statsadminCache statistics
POST/api/mods/cache/cleanadminClean cache

Configuration

MethodPathPermissionDescription
GET/api/servers/:id/configserver.configRead serverDZ.cfg
PATCH/api/servers/:id/configserver.configUpdate serverDZ.cfg
GET/api/servers/:id/config/templatesserver.configList templates
POST/api/servers/:id/config/templatesserver.configSave template
POST/api/servers/:id/config/templates/:templateId/restoreserver.configRestore from template
DELETE/api/servers/:id/config/templates/:templateIdserver.configDelete template

Backups

MethodPathPermissionDescription
GET/api/servers/:id/backupsAny authList all backups
POST/api/servers/:id/backupsserver.restartTrigger manual backup
DELETE/api/servers/:id/backups/:filenameserver.restartDelete backup. Query: `?type=manual
GET/api/servers/:id/backups/:filename/downloadserver.restartDownload backup zip
POST/api/servers/:id/backups/:filename/restoreserver.restartRestore backup
GET/api/servers/:id/backups/:filename/contentsserver.restartPreview backup contents
GET/api/servers/:id/backup-configAny authGet backup configuration
PUT/api/servers/:id/backup-configserver.restartUpdate backup config (interval, retention, paths)

Config Backups

MethodPathPermissionDescription
GET/api/backup/:typeadminDownload config backup (servers/users/roles/webhooks)
POST/api/restore/:typeadminRestore config from backup JSON

Deployment & Dangerzone

MethodPathPermissionDescription
POST/api/deployserver.deployDeploy new server via SteamCMD
POST/api/servers/:id/rebuildserver.deployRebuild server
GET/api/servers/:id/dangerzone/wipe-presetsadminWipe presets
POST/api/servers/:id/dangerzone/wipeadminExecute wipe
GET/api/servers/:id/dangerzone/logs-scanadminScan logs
POST/api/servers/:id/dangerzone/clear-logsadminClear logs
POST/api/servers/:id/dangerzone/replicate-previewadminPreview replication
POST/api/servers/:id/dangerzone/replicateadminExecute replication

Files

MethodPathPermissionDescription
GET/api/servers/:id/filesserver.configList files
GET/api/servers/:id/files/readserver.configRead file
PUT/api/servers/:id/files/writeserver.configWrite file

Items & Types Editor

MethodPathPermissionDescription
GET/api/servers/:id/itemsserver.configList items from types.xml
GET/api/servers/:id/types/filesserver.configList types files
GET/api/servers/:id/types/itemsserver.configLoad types items
GET/api/servers/:id/types/limitsserver.configLoad limits
PUT/api/servers/:id/types/saveserver.configSave items
POST/api/servers/:id/types/addserver.configAdd item
DELETE/api/servers/:id/types/itemserver.configDelete item

Logs & Metrics

MethodPathPermissionDescription
GET/api/servers/:id/logsserver.viewGet server logs
GET/api/servers/:id/consoleserver.viewGet console output
GET/api/servers/:id/metricsserver.viewGet metrics history

Priority Queue (VIP)

Automated VIP system that syncs entries to DayZ's native priority.txt file. Players in the queue get priority position in the login queue. Entries support time-limited expiration and are automatically cleaned every 60 seconds.

MethodPathPermissionDescription
GET/api/priority-queuepriority.manageList entries
POST/api/priority-queuepriority.manageAdd entry. Body: { "steamId", "name", "role", "expiresAt" }
PATCH/api/priority-queue/:idpriority.manageUpdate entry. Body: { "name", "role", "expiresAt" }
DELETE/api/priority-queue/:idpriority.manageRemove entry. Cleans all server priority.txt files
GET/api/priority-queue/exportpriority.manageExport all entries as downloadable JSON
POST/api/priority-queue/importpriority.manageImport entries from JSON array. Returns { added, skipped, errors }
POST/api/priority-queue/cleanuppriority.manageManually trigger expired entry cleanup. Returns { removed, remaining }

Add entry example:

{
  "steamId": "76561198012345678",
  "name": "PlayerName",
  "role": "VIP",
  "expiresAt": "2026-04-06T23:59:59.999Z"
}

Roles: VIP, Supporter, Premium

Expiration: Set expiresAt to an ISO date string for time-limited VIP, or null for permanent access.

Import format (POST body is a JSON array):

[
  {
    "steamId": "76561198012345678",
    "name": "PlayerName",
    "role": "VIP",
    "expiresAt": null,
    "addedBy": "admin"
  }
]

Webhooks

MethodPathPermissionDescription
GET/api/webhooksadminList webhooks
POST/api/webhooksadminCreate webhook
PATCH/api/webhooks/:idadminUpdate webhook
DELETE/api/webhooks/:idadminDelete webhook
GET/api/webhooks/:id/deliveriesadminDelivery records
POST/api/webhooks/:id/testadminTest webhook
GET/api/webhooks/eventsadminList event types

Notifications

MethodPathPermissionDescription
GET/api/notificationsAny authList notifications
PATCH/api/notifications/readAny authMark read
DELETE/api/notificationsAny authClear all

Users & Roles

MethodPathPermissionDescription
GET/api/usersadminList users
POST/api/usersadminCreate user
PATCH/api/users/:idadminUpdate user
DELETE/api/users/:idadminDelete user
GET/api/rolesadminList roles
POST/api/rolesadminCreate role
PATCH/api/roles/:idadminUpdate role
DELETE/api/roles/:idadminDelete role

License

MethodPathPermissionDescription
GET/api/licenseAny authGet license status
GET/api/license/tiersAny authGet tier info
POST/api/license/activateadminActivate key
DELETE/api/licenseadminDeactivate

Cloud Integration

MethodPathPermissionDescription
GET/api/cloud/statusadminGet cloud status
POST/api/cloud/enableadminEnable cloud
POST/api/cloud/disableadminDisable cloud
POST/api/cloud/connect/:serverIdadminConnect server
POST/api/cloud/disconnect/:serverIdadminDisconnect server
POST/api/cloud/reconnect/:serverIdadminReconnect server

Steam & Workshop

MethodPathPermissionDescription
GET/api/steam/statusAny authSteam status
POST/api/steam/credentialsadminSet credentials
POST/api/steam/credentials/saveadminSave without validation
GET/api/workshop/searchAny authSearch workshop
GET/api/workshop/details/:idAny authMod details
GET/api/workshop/popularAny authPopular mods

VIP Store

MethodPathPermissionDescription
GET/api/store/statusNoneStore status (public)
GET/api/store/productsNoneList products (public)
POST/api/store/checkoutAny authCreate Stripe checkout
POST/api/store/webhookNoneStripe webhook
GET/api/store/admin/productsadminAdmin: list products
POST/api/store/admin/productsadminAdmin: create product
PATCH/api/store/admin/products/:idadminAdmin: update product
DELETE/api/store/admin/products/:idadminAdmin: delete product
GET/api/store/admin/purchasesadminAdmin: list purchases
GET/api/store/admin/stripe-configadminAdmin: Stripe config
POST/api/store/admin/stripe-configadminAdmin: save Stripe config

System

MethodPathPermissionDescription
GET/api/system/infoadminSystem info
GET/api/system/metricsadminSystem metrics
GET/api/system/serviceadminService status
GET/api/system/configadminSystem config
PATCH/api/system/configadminUpdate config

Health

MethodPathPermissionDescription
GET/api/healthNoneComprehensive health check
GET/api/health/pingNoneLightweight ping

Audit

MethodPathPermissionDescription
GET/api/auditadminView audit trail

Scheduler

MethodPathPermissionDescription
GET/api/servers/:id/schedulerAny authList scheduled jobs
POST/api/servers/:id/schedulerserver.restartCreate a job (restart/stop, warnings, lock, kick)
PUT/api/servers/:id/scheduler/:jobIdserver.restartUpdate a job
PATCH/api/servers/:id/scheduler/:jobId/toggleserver.restartToggle job enabled/disabled
DELETE/api/servers/:id/scheduler/:jobIdserver.restartDelete a job

Watchlist

MethodPathPermissionDescription
GET/api/watchlistAny authList entries
POST/api/watchlistAny authAdd entry
DELETE/api/watchlist/:idAny authRemove entry

Additional Endpoints

MethodPathPermissionDescription
GET/api/killfeedAny authKill feed data
GET/api/leaderboardAny authPlayer leaderboard
GET/api/servers/:id/map/*Any authLive map data

Integrations / Discord bot

These endpoints are how the official Citadel Discord bot drives the controller. They have their own auth contract — see the Discord Bot guide for the full 3-layer security model.

POST /api/discord/action

Executes a Discord-initiated action against a Citadel server. The bot signs each call with HMAC-SHA256 over (timestamp, action, discordUserId) using the shared bot secret.

Headers:

Authorization: Bearer <DISCORD_BOT_API_KEY>
X-Discord-Ts:  <unix-seconds>
X-Discord-Sig: <hex-hmac-sha256>

Body:

{
  "action": "server.restart",
  "discordUserId": "242109987654321098",
  "discordUserTag": "moderator#0001",
  "serverId": "srv_4f3a",
  "params": { /* action-specific */ }
}

The backend resolves the calling Discord user against data/discord-user-roles.json (Layer 3) → falls back to the built-in discord-bot role (Layer 1). If the resulting role doesn't grant the permission ACTION_PERMISSIONS[action] requires, the call is rejected with 403 and a discord.denied audit row.

Stale or replayed signatures (timestamp older than the configured window, or HMAC mismatch) return 403 with discord.sig-rejected. Calls that don't carry the HMAC headers at all still execute (legacy bots) but get tagged as "Discord Bot (unverified)" in the audit log.

Per-Discord-user role mapping

MethodPathPermissionDescription
GET/api/discord/user-rolesusers.manageList all per-Discord-user role mappings.
PUT/api/discord/user-roles/:discordUserIdusers.manageBody { "role": "moderator" }. Sets/replaces the mapping. Logged as discord.user-role.set.
DELETE/api/discord/user-roles/:discordUserIdusers.manageRemoves the mapping (user falls back to the discord-bot role floor). Logged as discord.user-role.remove.

Rate limits

The API enforces a per-IP global cap plus tighter scoped caps on auth and bot paths. All limits are per-IP (not per-user) and use a 1-minute sliding window unless noted.

ScopeLimitNotes
Default /api/*600 / minGlobal ceiling on any /api endpoint not covered by a tighter scope.
/api/auth/*15 / 15 minLogin + MFA + password-reset endpoints. Per-(IP, username) lockout escalates further: 5 failed sign-ins triggers 60s → 5min → 1h bans (see Login lockout below).
/api/discord/*60 / minBot-driven action volume. The bot's per-Discord-user cooldowns (in the Discord bot guide) sit on top of this.
/api/maps/tiles/*exemptMap tiles are served at burst from browser caches; rate-limiting them would break the live-map UI on busy panels.

A request that trips a limit returns 429 Too Many Requests with a Retry-After header in seconds.

Login lockout

Failed sign-ins are tracked per (IP, username) rather than per-username alone, so an attacker who knows a target's username can't lock that account from arbitrary IPs. Five failures within 10 minutes from the same (IP, username) triggers a fail2ban-style escalating ban: first lockout 60s, second 5min, third+ 1h. A successful sign-in clears the counter.

This means: you'll never get locked out of your own account because someone else is spamming wrong passwords for it from a different IP.