The API

Start, stop and read your servers from a script, a Discord bot or Home Assistant. One key from your account page, one URL, plain JSON.

Updated September 2026

It's the web dashboard, as an API. A call goes to us, we pass it to the machine that runs the server over the same connection the web dashboard uses and the app answers. So the machine has to be on and the app open (or the Linux agent running), the same as for the dashboard. Nothing has to be port forwarded and nothing listens on your PC.

Keys

The API comes with Standard (bought outright, from the Microsoft Store, or included with Plus, Relay Starter or a cloud server) and with a Linux subscription. Make a key on your account page under Billing and keys. Give it a name (what will use it, so the audit log reads well), a level and, if you like, an expiry and a list of servers. The key is shown once. Copy it then, it can't be shown again, only revoked.

Every request carries it as a bearer token:

Authorization: Bearer mcsm_...

The three levels are the roles the dashboard already has, so a key can never do something a person with that role couldn't:

LevelWhat it reaches
Read and controlEvery read, start, stop, restart, kill, console commands, kicking a player
ManageThe above plus the settings, properties, backups, players, mods, plugins, the schedule and tasks
Full accessEverything you can do yourself, including deleting servers, cloning, importing, the relay and staff accounts

A key with a server list reaches those servers and nothing else. A key without one reaches every server on the account, including ones you make later. Either way, every call is written to the server's audit log under the key's name, so "who restarted it at 4am" has an answer.

If you're polling, every ten seconds is plenty. For anything that happens on the server (a start, a crash, a player joining) read the event feed with a long poll, or use the webhook on the Notifications tab, which posts to any URL the moment it happens.

Tiers

The API has tiers. Which one you're on decides how hard you can hit it. Lite has no API at all. Standard is what a Standard licence, Plus, Relay Starter, a cloud server or a Linux subscription gives you. It's the one nearly everyone is on. Enterprise is for a hosting company running many customers through the API: it isn't on sale yet, so until it is an account goes on it by agreement (email us, the terms say what that covers).

TierWho's on itRequests a minute, per keyKeys an account
LiteThe free app on its ownNo APINone
StandardStandard, Plus, Relay Starter, a cloud server, a Linux subscription12010
EnterpriseHosting companies, by agreement until the plan is on sale1,200100

Every answer carries X-Api-Tier and X-RateLimit-Limit headers. /v1/me says the tier in words, so a client can read its ceiling without guessing. Over the limit is a 429 with the limit in the message. The event feed's long poll counts as one request however long it waits, so a bot that listens rather than polls uses a few calls a minute.

Talking to it

The base is https://api.mcservermanager.com/v1. Requests and replies are JSON. A reply is the same data the web dashboard shows for that tab, so what you see on the dashboard is what you get here.

Errors are a status and a body with a sentence and a code:

{ "error": "The machine that runs this server is offline. Open MC Server Manager there, or check the agent.", "code": "machine_offline" }
StatusCodeMeans
401invalid_keyNo key, or one we don't recognise. Check the header
401key_revoked key_expiredThe key was revoked or has expired. Make a new one
403standard_requiredThe account no longer has Standard or a Linux subscription. The keys come back the moment it does
403no_accessThe key's server list doesn't include that server
403forbiddenThe key's level doesn't allow that operation
403standard_onlyThat's a Standard feature and the machine is on Lite
404unknown_serverNo server with that id on the account (ids are on GET /v1/servers)
409refusedThe machine refused the action, the body says why (already running, port in use, stop it first)
426update_appThe app on that machine is older than the API. Update to 4.0 or newer, or the latest Linux agent
429rate_limitedOver your tier's limit on this key, 120 a minute on Standard. The message says the number. Slow down
503machine_offlineThe machine isn't connected. Open the app there or check the agent
504no_replyThe machine didn't answer within 30 seconds. It may still be doing what you asked

Who am I

GET/v1/me answers the account's email, the key's own row (name, prefix, level, server list, when it was made, when it expires) and the tier the key is on, with its requests a minute and keys an account. Handy for checking a key works.

Servers

GET/v1/servers lists every server the key can reach, on every machine on the account:

{ "servers": [ {
  "id": "a1b2c3d4-...", "name": "Survival", "type": "Paper", "version": "1.21.1", "port": 25565,
  "running": true, "starting": false, "asleep": false, "players": 3, "online": true,
  "group": "Network", "machine": { "kind": "pc", "name": "Gaming PC", "id": "pc:4f1c..." }, "chat": true
} ] }

online is whether the machine is connected right now. machine.kind is pc for a Windows PC running the app, linux for a machine running the agent, cloud for a server we host. asleep is the sleep-when-empty state, a start or a player joining wakes it. group is the server list group it's in, null for none. machine.id names the machine for the network routes. chat is whether the app on that machine has the chat bridge.

GET/v1/servers/{id} is one row from that list.

Start, stop and the console

RouteSendsAnswersLevel
POST/v1/servers/{id}/startnothing{ "ok": true, "action": "start" }, or 409 with why notRead and control
POST/v1/servers/{id}/stopnothingthe sameRead and control
POST/v1/servers/{id}/restartnothingthe sameRead and control
POST/v1/servers/{id}/killnothingthe same. Kill ends the process without saving, stop is the normal wayRead and control
POST/v1/servers/{id}/command{ "command": "say Restart in 5 minutes" }{ "ok": true }. The command's output lands on the consoleRead and control
GET/v1/servers/{id}/console?lines=100up to 300 lines{ "lines": [ "[12:00:01] [Server thread/INFO]: Done (3.2s)!", ... ] }, the newest lastRead and control

A start answers when the process is launched, not when the server is ready. Watch the console for the Done line, or GET /v1/servers/{id} for running.

Players

GET/v1/servers/{id}/players answers who's on and the lists:

{ "onlinePlayers": [ "Steve" ], "whitelistEnabled": true,
  "whitelist": [ { "name": "Steve", "uuid": "..." } ], "ops": [ { "name": "Alex", "uuid": "...", "level": 4 } ],
  "bannedPlayers": [ { "name": "...", "reason": "...", "created": "..." } ], "bannedIps": [ { "ip": "...", "reason": "...", "created": "..." } ] }
RouteSendsLevel
POST/v1/servers/{id}/players/{name}/kick{ "reason": "..." }, optional. The server has to be runningRead and control
POST/v1/servers/{id}/players/{name}/ban{ "reason": "..." }, optionalManage
POST/v1/servers/{id}/players/{name}/unbannothingManage
POST/v1/servers/{id}/players/{name}/op and /deopnothingManage
POST/v1/servers/{id}/players/{name}/whitelist and /unwhitelistnothingManage

Each answers the updated players payload, so you don't need a second call to see the change. Bedrock servers keep their allowlist and permissions the same way, the routes are the same.

GET/v1/servers/{id}/players/history is who has ever played here, most recently seen first, from the join and leave lines the app reads off the console:

{ "now": 1789574400, "count": 2, "players": [
  { "name": "Steve", "xuid": null, "firstSeen": 1789400000, "lastSeen": 1789574000, "sessions": 12, "playSeconds": 43200, "online": true },
  { "name": "Alex", "xuid": null, "firstSeen": 1789300000, "lastSeen": 1789500000, "sessions": 3, "playSeconds": 5400, "online": false } ] }

Times are unix seconds. A player who's on now has their current session counted in playSeconds up to now. Bedrock players carry their xuid. The list holds the last 500 people seen.

Resources

GET/v1/servers/{id}/resources is the live reading, what the Resources tab shows:

{ "cpu": 12.4, "ramUsageMB": 2140, "ramAllocatedMB": 4096, "ramPercent": 52, "heapUsedMB": 1830, "heapMaxMB": 4096,
  "machineRamMB": 32768, "isStarting": false, "uptimeSeconds": 8123, "uptime": "2h 15m", "diskUsage": "1.2 GB",
  "tps": 20, "mspt": 4.1, "entityCount": 312, "loadedChunks": 890, "hasTpsData": true }

tps, mspt, entityCount and loadedChunks need the stats mod the app installs on Paper, Forge and Fabric, hasTpsData says whether they're real. heapUsedMB is null until the JVM has answered once.

GET/v1/servers/{id}/resources/history?hours=24 is the last day, one reading every 30 seconds while the server ran, averaged down to about 120 points unless you add &full=1:

{ "hours": 24, "full": false, "now": 1789574400, "stepSeconds": 30, "keepHours": 24, "count": 2880, "hasTps": true,
  "samples": [ [ 1789488000, 11.2, 2100, 20, 2 ], ... ] }

Each sample is [unix time, cpu percent, ram MB, tps or null, players on]. A gap in the times is a gap in running, the server was off.

Backups

RouteDoesLevel
GET/v1/servers/{id}/backupsthe list: backups and preRestore, each { fileName, sizeMB, createdDate, isValid, validationError }, plus autoBackupOnStop, localKeep, cloudKeepRead and control
POST/v1/servers/{id}/backupsmakes one. It runs in the background and answers straight away, poll the list for it. Works on a running server, the world is held still while it zipsManage
POST/v1/servers/{id}/backups/{fileName}/restorerestores that one. The server has to be stopped. A safety copy of what was there goes into preRestore firstManage
DELETE/v1/servers/{id}/backups/{fileName}deletes that oneManage
POST/v1/servers/{id}/backups/foldersets where the backups are written on the machine: { "folder": "D:\\Backups\\Survival" }, the full path of an empty folder outside the server, on another drive so a dead disk doesn't take the backups with it. The backups already there move with it. An empty string puts them back in the server's own folder. The list answers with backupsFolder (as set) and backupsDir (in use)Full access

Properties

GET/v1/servers/{id}/properties answers server.properties as { "properties": { "motd": "...", "max-players": "20", ... } }, every value a string as it is in the file.

PATCH/v1/servers/{id}/properties with { "properties": { "motd": "Back at 6", "max-players": 30 } } changes just those and leaves the rest alone, then answers the whole file. Most properties are read at the next start.

Schedule and tasks

GET/v1/servers/{id}/schedule is the Schedule tab: the restart mode, the times, restart when empty, the crash restart setting, sleep when empty.

GET/v1/servers/{id}/tasks is the task list, each with its trigger, its action, what it says in words (whenText, whatText) and when it last ran. POST/v1/servers/{id}/tasks/{taskId}/run runs one now (Manage).

Worlds, mods and plugins

GET/v1/servers/{id}/worlds, GET/v1/servers/{id}/mods and GET/v1/servers/{id}/plugins answer the lists those tabs show: worlds with their size and which one is active, mods and plugins with their file name and whether they're enabled.

Updates

Newer builds of what's installed, from Modrinth and CurseForge, never outside the server's own Minecraft version and loader. A candidate has to list the server's exact version and, for a mod, its loader, or it isn't offered. The app knows where it installed each jar from. A jar you dropped in yourself is matched by its hash where the sites know it.

RouteDoesLevel
GET/v1/servers/{id}/updateschecks every installed mod or plugin, the answer is below. A site that can't be reached puts its rows in failed, never in upToDateManage
POST/v1/servers/{id}/updatesapplies updates. Body { "fileName": "sodium-0.5.jar" } for one, an empty body for all of them. The server has to be stopped, a jar in use can't be replaced. The check runs again first, so what's applied is what the sites offer right now. Answers { done: [ { fileName, newFileName, name, version } ], failed: [ { fileName, error } ], summary }. Updating everything on a big server can take longer than the 30 second window, then you get a 504 while it carries on and the next check shows what landedManage
GET/v1/servers/{id}/modpackthe CurseForge pack this server was made from and the newest build of it for the same Minecraft version, or tracked: false for a server not made from a pack through the appManage
POST/v1/servers/{id}/modpack/updatemoves the server to that build: { "fileId": 6543210 } from the check. A backup is made first, the pack's mods and configs are replaced, the world, server.properties, ops, whitelist and bans are kept, mods you added yourself go with the old build and are in the backup. A build for another Minecraft version is refused. It runs in the background and answers straight away, the console shows each stepManage
{ "kind": "mod", "checkedAt": "2026-09-16T20:00:00Z", "checked": 3, "summary": "1 has an update",
  "updates": [ { "fileName": "fabric-api-0.100.0+1.21.1.jar", "name": "Fabric API", "provider": "modrinth", "projectId": "P7dR8mSH",
                 "installedVersion": "…", "latestVersion": "0.110.0+1.21.1", "latestId": "…", "latestFileName": "fabric-api-0.110.0+1.21.1.jar", "datePublished": "…" } ],
  "upToDate": [ "sodium-0.6.0.jar", "lithium-0.13.0.jar" ], "unknown": [ "homemade.jar" ], "failed": [] }

kind is mod or plugin from the server's type. unknown is jars from neither site, they can't be checked. The pack check answers { tracked, current: { provider, projectId, fileId, name, version }, latest: { fileId, name, version, datePublished, releaseType, serverPack } | null, error }.

Server List Beta

Your servers on the Server List, the public list of servers run with the app. What you write is yours, whether it's running and how many are on is read off the server. A listing made here is the same one the app and the web dashboard edit.

RouteDoesLevel
GET/v1/nexusyour listings and the servers you could list. A key limited to some servers only sees thoseRead and control
POST/v1/nexus/{id}lists the server, or updates its listing. The body is belowFull access
DELETE/v1/nexus/{id}takes it off the listFull access
{ "title": "Oak Valley SMP", "description": "A friendly survival server, no resets, land claims on.",
  "tags": [ "Survival", "SMP" ], "address": "play.example.net",
  "discord": "discord.gg/your-invite", "website": "example.net",
  "votePrompt": true, "votePromptHours": 4, "votePromptDaily": 1, "sharePlayers": false }

A title of 3 to 60 characters, a description of 20 to 500, one to five tags from Survival, SMP, Creative, Vanilla, Modded, Hardcore, Skyblock, Minigames, PvP, Roleplay, Economy, Crossplay, Whitelist, Family friendly or 18+. Then the address players type to join (a name or an IP, with :port if it needs one). discord has to be an invite and website a web address, both optional. Five listings an account.

votePrompt has the app tell each player in game, once they've been on ten minutes, that they can vote for the server, no more often than every votePromptHours (2 to 24) and at most votePromptDaily times a day (1 to 5). sharePlayers adds who plays here to the shared player record. Both are off unless you turn them on. Leaving either out of an update keeps what's there. The answer is { success, listing, url }. An account that hasn't verified its email is refused with unverified.

Network actions

A group of servers, or every server the key reaches, at once and across every machine on the account. A group is the server list group, by name in any case, on every machine that has one called that. A backend linked to a proxy counts as in the proxy's group. Each route takes group and machine (a machine.id from the server list) to narrow it, in the body for a POST and the query for a GET. Leave both out for every server.

RouteDoesLevel
GET/v1/network/groupsthe groups, each { name, servers, running, machines: [ { kind, name } ] }, plus the run going on now if this key may read itRead and control
POST/v1/network/broadcastshows { "message": "Restart in 5 minutes", "group": "Network" } in the chat of every running server. Java and Bedrock servers get say, a BungeeCord proxy gets alert. A proxy is left out when game servers in the same send carry the message. Velocity has no command for it. Answers a result per server, belowRead and control
GET/v1/network/players?q=notchfinds a player by name, any part of it, at least 2 letters, in every server's player history on every machine. BelowRead and control
POST/v1/network/startstarts the stopped servers in network order: game servers first, the proxies last. Answers 202 with the runRead and control
POST/v1/network/stopstops the running servers, the proxies first. 202 with the runRead and control
POST/v1/network/restartstops the running servers in stop order, then starts the same ones in start order. 202 with the runRead and control
GET/v1/network/runs/{runId}the run as it standsRead and control
POST/v1/network/runs/{runId}/cancelstops the run where it is. What was already sent isn't undone, the server it's waiting on is still reported as it lands and `cancelling` is true until thenRead and control
{ "message": "Restart in 5 minutes", "counts": { "sent": 2, "skipped": 1, "offline": 1 }, "results": [
  { "id": "...", "name": "Hub", "machine": "Gaming PC", "status": "skipped", "reason": "backends" },
  { "id": "...", "name": "Lobby", "machine": "Gaming PC", "status": "sent", "reason": null },
  { "id": "...", "name": "Minigames", "machine": "Linux box", "status": "offline", "reason": "machine" } ] }

A status is sent, offline (the server isn't running, or reason: "machine" when its machine is off), skipped (backends, noCommand for Velocity, update for an app or agent from before network actions), denied, failed or missing.

{ "query": "notch", "players": [
  { "name": "Notch", "serverId": "...", "serverName": "Lobby", "serverType": "Paper", "machine": "Gaming PC", "machineKind": "pc:4f1c...",
    "online": true, "lastSeen": 1789574000, "firstSeen": 1789400000, "sessions": 12, "playSeconds": 43200, "rank": 0 } ],
  "machines": [ { "kind": "pc:4f1c...", "name": "Gaming PC", "status": "ok", "searched": 3 }, { "kind": "device:...", "name": "Linux box", "status": "offline", "searched": 0 } ] }

The whole name comes first (rank 0), then names starting with it (1), then names holding it (2). Within each, whoever's on now comes first, then the most recently seen. A machine that couldn't be searched is in machines with why, so a missing player is never taken as not found on a machine that wasn't asked. 200 results at most.

A run goes on in your account's hub, not in your script, so it finishes if the script stops. Each server is waited for before the next one goes: up and ready for a start (5 minutes at most), stopped for a stop (2 minutes at most), then the run moves on either way. One run at a time an account, a second answers 409 run_in_progress with the one going. A key reads and cancels the runs it started. A key at Full access with every server reads them all.

{ "run": { "id": "7d1e...", "action": "restart", "scope": { "group": "Network", "machine": null }, "state": "running",
  "createdAt": 1789574400000, "finishedAt": null, "cancelling": false, "counts": { "done": 1, "working": 1, "waiting": 2 },
  "steps": [ { "serverId": "...", "name": "Hub", "machine": "Gaming PC", "machineKind": "pc:4f1c...", "kind": "stop", "status": "done", "error": null, "message": null },
             { "serverId": "...", "name": "Lobby", "machine": "Gaming PC", "kind": "stop", "status": "working", ... },
             { "serverId": "...", "name": "Lobby", "kind": "start", "status": "waiting", ... },
             { "serverId": "...", "name": "Hub", "kind": "start", "status": "waiting", ... } ] } }

state is running, done or cancelled. A step's status is waiting, working, done (error: "already" when it was there before its turn), failed (error is refused with the machine's message, machineOffline, noReply, update or gone), timedOut, skipped (update, or notStopped for a restart's start whose stop didn't finish) or cancelled. Times are unix milliseconds. A server whose app or agent is from before network actions is skipped with update, update it and it joins in.

Everything else

The web dashboard does a lot more than the routes above: settings, mods on and off, worlds, the file manager, cloud backups, the relay, cloning. All of it is reachable as POST/v1/servers/{id}/op with the dashboard's own operation as the body, { "type": "scheduleSleep", "enabled": true, "minutes": 15 }. The key's level applies exactly as it does on the dashboard.

The catch: those names and fields are the dashboard's own, so they can change between versions without notice. The routes above are the promise, this is the escape hatch. If you find yourself relying on one, tell us and we'll give it a route.

Events

What happened, for a bot that wants to say so: a start, a stop, a crash, a player joining or leaving, a scheduled task. The machine pushes them to us while something is reading, we keep the last 200 an account and you read them by sequence number. Nothing is pushed while nobody's asking, so an account nobody polls costs nothing.

RouteAnswersLevel
GET/v1/servers/{id}/events?since=&wait=the events on that serverRead and control
GET/v1/events?since=&wait=the events on every server the key reaches, on every machineRead and control
GET/v1/stream?events=the same events pushed down a WebSocket as they land, see belowRead and control

The first call goes without since and answers next only, nothing stored, so a bot that starts up doesn't replay last night. Then poll with since set to the next you were given and you get everything after it, oldest first, up to 100 at a time, with a new next. Keep next and you never miss one or see one twice, even across your own restarts. since=0 is everything we have.

wait is a long poll: with nothing new, the call holds for up to that many seconds (25 at most) and answers the moment an event lands, or empty when the time's up. A bot on wait=25 in a loop hears about a join within a second and makes three calls a minute doing it.

For a bot or a script that stays connected, GET /v1/stream?events= does the same over a WebSocket and is the better choice, events come down it the moment they land with no polling at all. Open it with your key as Authorization: Bearer mcsm_... and events set to the next you were last given, or now. You get everything after it first, then each event as it happens, as { "t": "events", "events": [ ... ], "next": 419 }, the same shape a poll answers. Keep next as before and a reconnect picks up where it left off. Send {"t":"ping"} every 30 seconds to keep it open and {"t":"renew"} every few minutes, the renew is what keeps the machines pushing. We close a stream after an hour (4001) or when its key is revoked (4003) and say so first in a last { "t": "closed", "code": 4001, "reason": "reconnect" }, so dial again and carry on. Without the upgrade the route answers 426.

{ "events": [ {
  "seq": 418, "at": 1789574400, "serverId": "a1b2c3d4-...", "event": "playerJoined",
  "server": { "name": "Survival", "type": "Paper", "version": "1.21.1", "address": "play.example.com:25565", "port": 25565, "players": 3, "uptimeSeconds": 5400 },
  "data": { "title": "Player Joined", "text": "Steve joined Survival", "player": "Steve", "players": ["Steve"] },
  "kind": "pc:..."
} ], "next": 418 }

The names and the server block are the webhook's, so a bot reads one shape whichever way an event reached it. Every event's data carries title and text, the same words the webhook and the Discord embed use, so a bot can post text as it is.

EventAlso in data
started restarting stoppednothing more
crashedexitCode
playerJoined playerLeftplayers (the names, a list, since a few joining at once come as one event) and player (the first of them)
tasknothing more, the title and text are a scheduled task's Send a notification, as you wrote it

at is unix seconds, when it happened on the machine. kind is which machine sent it. address is what players connect to, null while the machine can't say. server.players is how many are on, uptimeSeconds is null when the server isn't running.

The app on the machine has to be 4.0 or newer (or the Linux agent its latest release) for events to flow. An older one still answers the routes, it just never pushes anything into them. The machine pushes only while something is polling, so keep polling: a bot that stops for an hour and comes back starts fresh with next, what happened in between wasn't pushed. If you'd rather be told than ask, the webhook on a server's Notifications tab posts the same events to any URL the moment they happen, signed with a secret if you set one.

Chat

The game's chat, for a bridge to Discord or anywhere else, with no plugin or mod on the server. The app on the machine reads chat, joins, leaves, deaths and advancements off the console and pushes them to us while something is reading, the same way the events work. It's a feed of its own beside the events, so a busy chat never pushes a crash out of yours.

RouteSendsAnswersLevel
GET/v1/chat?since=&wait=nothingchat on every server the key reachesRead and control
GET/v1/servers/{id}/chat?since=&wait=nothingchat on that serverRead and control
POST/v1/servers/{id}/chat{ "author": "Bob", "text": "hello from Discord" }{ "ok": true, "via": "tellraw" }Read and control

Reading works exactly like events: the first call without since answers next only, then poll with since and wait up to 25. We keep the last 300 lines an account, up to 100 come back a call.

{ "chat": [ {
  "seq": 77, "at": 1792324800, "serverId": "a1b2c3d4-...", "type": "chat",
  "player": "Steve", "text": "anyone up for the nether?", "kind": "pc:..."
} ], "next": 77 }
typetext
chat emotewhat the player said (an emote is /me)
join leaveempty, player says who
death advancementthe game's own line, Steve was slain by Zombie

Vanilla, Paper, Purpur, Spigot, Fabric, Quilt, Forge and NeoForge chat is read in all their console shapes. Bedrock prints joins and leaves, plus its chat only when the server itself prints it. A chat plugin that changes the console's chat line can hide chat from it.

Sending puts one line in the game for everyone, shown as [Discord] Bob: hello from Discord. Java gets tellraw, a server older than 1.7.2 gets say, Bedrock gets its own tellraw. The text is made one line, colour codes and invisible characters come out, text is cut at 256 characters and author at 32. The app builds the command itself, so nothing in the text can run as a command of its own. A proxy has no chat, a stopped server answers 400 with why and an app on the machine from before the chat bridge answers 426 update_app. GET /v1/servers says which servers can bridge: "chat": true on the row.

Examples

Start a server from a shell:

curl -X POST https://api.mcservermanager.com/v1/servers/SERVER_ID/start \
  -H "Authorization: Bearer mcsm_..."

Restart every night at 4am, one line in crontab:

0 4 * * * curl -s -X POST https://api.mcservermanager.com/v1/servers/SERVER_ID/restart -H "Authorization: Bearer mcsm_..."

Who's on, in Python:

import requests

API = "https://api.mcservermanager.com/v1"
H = {"Authorization": "Bearer mcsm_..."}

for s in requests.get(f"{API}/servers", headers=H).json()["servers"]:
    if not s["running"]:
        continue
    players = requests.get(f"{API}/servers/{s['id']}/players", headers=H).json()
    print(s["name"], players["onlinePlayers"])

A Discord bot command in Node (discord.js), stopping the server if nobody's on:

const API = 'https://api.mcservermanager.com/v1';
const headers = { Authorization: 'Bearer mcsm_...', 'Content-Type': 'application/json' };

async function stopIfEmpty(serverId) {
  const players = await fetch(`${API}/servers/${serverId}/players`, { headers }).then(r => r.json());
  if (players.onlinePlayers.length > 0) return `${players.onlinePlayers.length} still on, leaving it`;
  const res = await fetch(`${API}/servers/${serverId}/stop`, { method: 'POST', headers }).then(r => r.json());
  return res.ok ? 'Stopping' : res.error;
}

The same bot's feed, one long poll after another, posting each event to a channel:

let next = (await fetch(`${API}/events`, { headers }).then(r => r.json())).next;
for (;;) {
  const page = await fetch(`${API}/events?since=${next}&wait=25`, { headers }).then(r => r.json());
  for (const ev of page.events) {
    if (ev.event === 'playerJoined') channel.send(`${ev.data.players.join(', ')} joined ${ev.server.name} (${ev.server.players} on)`);
    else if (ev.event === 'crashed') channel.send(`${ev.server.name} crashed, exit code ${ev.data.exitCode}`);
    else channel.send(ev.data.text);
  }
  next = page.next;
}

Home Assistant, a button that starts the server (configuration.yaml):

rest_command:
  start_minecraft:
    url: https://api.mcservermanager.com/v1/servers/SERVER_ID/start
    method: POST
    headers:
      Authorization: "Bearer mcsm_..."

And a sensor for the player count, read every minute:

sensor:
  - platform: rest
    name: Minecraft players
    resource: https://api.mcservermanager.com/v1/servers/SERVER_ID
    headers:
      Authorization: "Bearer mcsm_..."
    value_template: "{{ value_json.players }}"
    json_attributes: [running, online, name]
    scan_interval: 60

Limits worth knowing

  • The machine has to be connected. A PC that's asleep or an app that's closed answers 503, the same as the dashboard would show it offline.
  • 30 seconds for an answer. Almost everything answers in well under a second, a backup of a big world or a restore can take longer and answers as soon as it's started.
  • 120 requests a minute a key and ten keys an account on Standard, more on Enterprise. Poll gently, the event feed's long poll waits for you so a bot needs three calls a minute, not sixty.
  • The settings reply never carries the machine's IP addresses to a key. If a script needs the address players use, it's on the Network tab of the dashboard. The relay address is in the settings reply when a relay is on.

Something missing? If a route you need isn't here, or a reply doesn't carry a field you'd expect, tell us. The dashboard already does most things, giving one a route is usually quick.