#Web API
Every endpoint is at http://your-server-ip:port/upgrade-admin/... on your game server's normal port - that's not a configurable "API path", it's FXServer's own SetHttpHandler routing on the resource's folder name, so it changes if you rename the resource folder.
Plain HTTP. An unmodified FXServer speaks HTTP, not HTTPS, so the examples below use
http://. That means the token and every admin action cross the network unencrypted. If you put your server's HTTP port behind a TLS reverse proxy, usehttps://here and tick "Connect over HTTPS" on the dashboard.
#Authentication
Requests carry two things, and they answer two different questions.
The Bearer token says which server this request belongs to. There is one token per server, generated the first time the resource starts. It does not grant a permission level: Config.API.ServerTokenRole is nil by default, so a request holding only the token can reach the endpoints that need no permission (GET /server, GET /me) and nothing else.
The signed acting headers say who is making the request, and that's where permissions come from. The role is resolved exactly like an in-game one - from an admin_roles row keyed by "discord:<their Discord ID>" instead of a licence. No row there means no permissions, same as an unrecognised player in-game.
Authorization: Bearer ua_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Acting-Id: 123456789012345678
X-Acting-Name: SomeAdmin
X-Acting-Timestamp: 1757116800
X-Acting-Signature: <hex HMAC-SHA256>
The signature is HMAC-SHA256("<X-Acting-Id>.<X-Acting-Timestamp>", dashboard_secret), hex encoded. The dashboard secret is printed alongside the API token when the resource generates them, and is the reason an identity claim can be trusted at all: the token is shared by everyone with dashboard access, so an unsigned X-Acting-Id would let anyone holding it name a god admin's Discord ID and inherit that role.
An unsigned, mistyped or stale signature is not an error - the request simply falls back to the token's own role (i.e. no permissions by default), and a line is written to the server console saying so. Timestamps more than Config.API.ActingSignatureToleranceSeconds (default 300) from the server's clock are rejected.
In PHP, that signature is:
$timestamp = (string) time();
$signature = hash_hmac('sha256', $discordId . '.' . $timestamp, $dashboardSecret);
Connecting a server to the dashboard requires the connecting Discord account to already resolve to god - checked via GET /me before the dashboard saves the connection.
#Rate limits
Two independent sliding windows:
Config.API.RateLimitPerMinute(default 120) per acting admin, once authenticated.Config.API.RateLimitPerMinutePerAddress(default 300) per remote address, counted before authentication so failed token guesses are throttled too.
Exceeding either returns 429.
#GET /server
Server name, player count, framework, version, and the full metrics block (staff online, character/vehicle/ban counts, economy totals). The metrics block is only included for callers with the servermetrics permission; the rest needs none, which is what makes this usable as a health check.
curl -H "Authorization: Bearer $TOKEN" http://yourserver.com:30120/upgrade-admin/server
#GET /me
Resolves the caller - who you're acting as and what role you have. Useful for a client that just wants to say "connected as ___", and for checking whether your signature is being accepted (an unsigned request reports the token's own role, not yours).
#GET /players
Online + offline players, merged. Supports ?q=, ?page=, ?limit= (max 200 per page). Filtering and paging happen server-side, so this is safe to call on servers with thousands of characters.
#GET /players/:id
Player detail - :id can be a server ID (online) or a citizen ID (online or offline).
#PATCH /players/:id
Edit a player. Body can include cash, bank, job, jobGrade, gang, gangGrade, firstname, lastname. Money edits require the player to be online; job/gang/name work offline too.
#POST /commands/:action
The generic command dispatcher - this is how you kick, ban, warn, freeze, teleport, give money/items, set job/gang, and everything else. The full list of :action values mirrors every action the in-game panel can perform. A few of them (goto, bring, spectate, spawnvehicle, and similar) move the admin's own character or camera, so they only work when the acting admin is actually connected in-game.
curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"targetId": 1, "reason": "testing"}' \
http://yourserver.com:30120/upgrade-admin/commands/kick
For ban, a seconds value of 0 or less means a permanent ban.
#GET / POST /chat
Admin chat history and sending a message.
#GET / POST /tickets, PATCH / DELETE /tickets/:id
List/create tickets; PATCH with {"action": "claim"}, {"action": "close"}, or {"action": "reply", "message": "..."}.
#GET /bans
Active bans. An expire of 2147483647 means permanent.
#GET /admins, POST /admins, PATCH /admins/:identifier, DELETE /admins/:identifier
Full admin/permission management - the same thing the Users page does. PATCH accepts role and/or permissions (a full array that replaces that admin's custom permission set).
Role changes are also bounded by seniority: nobody can grant a role above their own, or modify an admin more senior than they are. See Permissions & Users.
#GET /permissions/catalog
The full permission catalog, for building a picker UI. Permission names sent to PATCH /admins/:identifier are validated against it, so a typo is rejected rather than silently stored.
#GET /server/resources, POST /server/resources/:name, GET /server/logs
Resource list/start/stop/restart, and the audit log. Resources listed in Config.ProtectedResources (the database bridge, the framework core, this resource itself) cannot be stopped or restarted through the API - stopping them would take the server, or the panel issuing the command, down with it.
#Real-time updates
Set Config.API.DashboardWebhookUrl to have the resource POST your own backend whenever a new admin chat message or ticket comes in, instead of polling. Those pushes carry an X-Upgrade-Signature header - an HMAC-SHA256 of the raw request body using the same dashboard secret - so your receiver can verify the push genuinely came from your server.