GameGrid

Command Manager API

Schedule recurring console commands from your own code - list, create, edit, toggle, run and audit schedules, with every field, limit and status a schedule can carry.

What these routes are for

These routes are the Command Manager without the panel. Anything you can build on the Command Manager tab - an hourly save, a five-step restart sequence with warnings, an item for every player online - you can create, edit and audit from a script instead.

They sit inside the Customer API and follow all of its conventions: same base URL, same bearer token, same error shape, same rate limits, same pagination.

Base URL: https://wdcgamegrid.com/api/public/v1

Read the catalog before you write anything. Command IDs, parameter names and the all-players selector are per game and are not guessable. GET /servers/{id}/command-catalog is the authoritative list for the server you are pointed at.

Your first request

Ask what this server can be told to do:

curl -H "Authorization: Bearer gsk_YOUR_KEY_HERE" \
https://wdcgamegrid.com/api/public/v1/servers/{serverId}/command-catalog

A five-minute first script:

1. GET /servers - your servers and their ids.

2. GET /servers/{id}/command-catalog - the commands this game supports, and their parameters.

3. POST /servers/{id}/command-schedules - create something harmless, like a world save.

4. POST /servers/{id}/command-schedules/{scheduleId}/run - fire it once, now, without waiting for the cron.

5. GET /servers/{id}/command-schedules/{scheduleId}/log - read what actually happened.

Step 4 then step 5 is the loop worth learning. It tells you whether a schedule works in seconds instead of at 4am tomorrow.

Scopes

ScopeGrants
command_schedules:readList and read schedules, read the command catalog, read execution history
command_schedules:writeCreate, update and delete schedules; toggle them on and off
command_schedules:controlRun a schedule immediately - this sends a command to a live server

All three are enabled by default on a new API key. Narrow them when you create or edit the key in the panel.

:control is the one to think about. A key that only reports on schedules needs :read and nothing else, and cannot then be used to fire a stop at your server.

Read the command catalog

GET /servers/{id}/command-catalog — scope command_schedules:read

Returns every command available for this server's game, grouped by category, with the parameters each one takes.

{
"gameKey": "seven-days-to-die",
"version": 2,
"allPlayersSelector": null,
"commands": [
{ "id": "saveworld", "label": "Save World", "syntax": "saveworld",
"description": "Saves the game world", "category": "server",
"warning": null, "params": [] },
{ "id": "buffplayer", "label": "Buff Player", "syntax": "buffplayer {player} {buff}",
"category": "player", "params": [
{ "key": "player", "label": "Player", "type": "text", "required": true, "playerTarget": true },
{ "key": "buff", "label": "Buff", "type": "text", "required": true } ] }
]
}
FieldMeans
allPlayersSelectorThe game's own word for "everyone", or null. "@a" on Minecraft; null on the other four
commands[].idWhat you send as commandType
commands[].syntaxThe template. {braces} are parameter keys
commands[].warningPresent when the command is destructive or behaves oddly on a schedule. Show it to your user
params[].playerTargettrue means this parameter takes a player, and can be expanded to everyone online

Catalog sizes differ per game, and a command that exists on one game may not exist on another:

GameCommandsallPlayersSelectorPlayer-target commands
7 Days to Die46null10
Windrose25null9
Minecraft24"@a"10
Palworld11null4
Valheim7null1

Windrose has no broadcast, say or chat command at all, so a Windrose schedule cannot message players. Do not assume every game has a way to announce something.

List schedules

GET /servers/{id}/command-schedules — scope command_schedules:read

Returns every schedule on this server.

FieldMeans
id, nameIdentity
commandThe rendered command string for a single-command schedule
command_type, command_paramsWhat it was built from
stepsnull on a single-command schedule; an ordered array on a multi-step one
cron_expression, timezoneWhen it fires, and in which timezone the cron is read
enabledWhether it fires at all
next_run_atThe next firing, computed server-side
last_run_at, last_run_status, last_run_output, last_errorThe most recent run
run_when, min_playersThe run condition
notify_onWhich outcomes raise an alert
overlap_policy, catchup_window_minutes, jitter_seconds, missed_toleranceReliability settings
consecutive_misses, last_run_ms, running_untilScheduler bookkeeping you can read

last_run_status

ValueMeans
successSent, and the server replied
unverifiedSent down a channel that carries no reply. Treat as "sent", not as "worked"
errorNot delivered, or the server rejected it
skippedA run condition turned this occurrence away. Not a failure
lateMore consecutive runs missed than missed_tolerance allows

unverified is normal on Minecraft, whose console returns nothing. It is honesty, not a fault - see Command Manager Troubleshooting.

Step objects

FieldMeans
commandType, commandParamsWhat this step runs
commandThe rendered string
delaySecondsSeconds to wait *before* this step. Ignored on the first step
targetAllPlayersWhether this step expands to every online player
playerParamKeyWhich parameter is the player target. Present only when targetAllPlayers is true

Create a schedule

POST /servers/{id}/command-schedules — scope command_schedules:write — returns 201 with the created schedule.

A single command

{
"name": "Daily World Save",
"commandType": "saveworld",
"commandParams": {},
"cronExpression": "0 4 * * *",
"timezone": "America/New_York",
"enabled": true
}

Several commands in order

Send steps instead of commandType. Up to 20, run sequentially on one trigger.

{
"name": "Daily Maintenance",
"steps": [
{ "commandType": "say", "commandParams": { "message": "Saving world..." } },
{ "commandType": "save-all", "commandParams": {} },
{ "commandType": "say", "commandParams": { "message": "World saved" } }
],
"cronExpression": "0 4 * * *",
"timezone": "America/New_York"
}

With waits between the steps

delaySeconds is how long to wait before that step. It is ignored on the first step, and capped at one hour.

{
"name": "Nightly Restart",
"steps": [
{ "commandType": "say",      "commandParams": { "message": "Restart in 30 minutes" } },
{ "commandType": "say",      "commandParams": { "message": "Restart in 10 minutes" }, "delaySeconds": 1200 },
{ "commandType": "save-all", "commandParams": {}, "delaySeconds": 540 },
{ "commandType": "stop",     "commandParams": {}, "delaySeconds": 60 }
],
"cronExpression": "30 3 * * *",
"timezone": "America/New_York",
"runWhen": "server_online",
"overlapPolicy": "skip",
"notifyOn": ["failure"]
}

A sequence that waits is still running while it waits. That is what overlapPolicy is for - four steps spanning half an hour really can still be going when the schedule next comes due.

Give to every online player

Set targetAllPlayers on a step and name which parameter is the player, then leave that parameter out of commandParams - the platform fills it in.

{
"name": "Daily Diamond Drop",
"steps": [
{ "commandType": "give",
"commandParams": { "item": "diamond", "count": 64 },
"targetAllPlayers": true,
"playerParamKey": "player" }
],
"cronExpression": "0 4 * * *"
}

playerParamKey must name a parameter the catalog marks playerTarget: true. Omit it and the first player-target parameter is used.

What happens at execution time depends on the game:

GameSendsIdentifier substituted
MinecraftOne command@a - the game resolves "everyone" itself
PalworldOne per playerPlatform ID (steam_, gdk_, ps5_)
7 Days to DieOne per playerEntity ID - digits, because a name can contain a space
ValheimOne per playerSession ID, which changes on every reconnect
WindroseOne per playerPlayer name - no numeric handle exists

Each expanded command gets its own row in the execution history, naming the player it ran for.

Nobody online is a no-op, not a failure. A roster that cannot be read is an error and nothing is sent - an unreachable server must never be mistaken for an empty one, or a giveaway reports success having given nothing to anybody.

Every field a schedule can carry

Everything below is accepted by both POST (create) and PATCH (update). On a PATCH, fields you leave out keep their current values.

FieldTypeDefaultNotes
namestringRequired. 1-100 characters
cronExpressionstringRequired. 5-field cron, up to 120 characters
timezoneIANA nameUTCThe timezone the cron is read in
enabledbooleantrueA disabled schedule stays stored and never fires
commandTypestringSingle-command schedules. Mutually exclusive with steps
commandParamsobject{}Values for that command's parameters
stepsarray1-20 step objects. Send null on a PATCH to make it single-command again
steps[].delaySecondsnumber0-3600. Wait before this step. Ignored on the first
steps[].targetAllPlayersbooleanfalseExpand this step to every online player
steps[].playerParamKeystringfirst matchWhich parameter is the player target
runWhenenumalwaysalways, server_online, players_online
minPlayersnumber or nullnull1-1000. A floor below which the run is skipped
notifyOnarray[]Up to 3 of failure, skipped, success
overlapPolicyenumskipskip, queue, allow - when it comes due mid-run
catchupWindowMinutesnumber150-1440. How late a run may still happen
jitterSecondsnumber00-900. Start inside a random window, not on the minute
missedTolerancenumber10-50. Consecutive misses before the schedule is flagged late

What the reliability fields actually do

FieldWhy it exists
overlapPolicyTwo overlapping restarts is a corrupted world. skip drops the occurrence; queue waits for the previous run; allow runs both
catchupWindowMinutesA restart arriving four hours after it was announced is worse than one that did not happen. Past the window the run is recorded as missed rather than fired late. 0 means "only at the moment it was due"
jitterSecondsTen servers that all save at 04:00 hit the same disk in the same second. Spread them
missedToleranceOne blip should not wake anybody. A schedule that has quietly stopped firing should be impossible to miss

Updating a schedule that had been flagged late clears the flag, so something you have just fixed stops reporting itself as broken.

Check a schedule before you save it

GET /servers/{id}/command-schedules/preview?cronExpression=&timezone=

Returns the expression as a sentence, plus the next five times it will actually fire.

{
"valid": true,
"description": "Every Monday at 4:00 AM",
"nextRuns": ["2026-09-14T08:00:00.000Z", "2026-09-21T08:00:00.000Z",
"2026-09-28T08:00:00.000Z", "2026-10-05T08:00:00.000Z",
"2026-10-12T08:00:00.000Z"],
"timezone": "America/New_York",
"error": null
}

nextRuns are ISO-8601 UTC timestamps, worked out on the server that will run the schedule. That is the whole point of the route: computing them again on the client gives a second opinion that agrees right up until a daylight-saving boundary.

An expression that will not parse, or that fires more often than the five-minute minimum, comes back with valid: false, an empty nextRuns, and the reason in error - the same message the create route returns as a 400.

This one is a panel route, not an API-key route. It lives under /api/v1/..., is authorised by your panel session rather than a gsk_ key, and is read-only. There is no API-key equivalent yet, so to validate an expression with a key, POST the schedule and read the 400.

Read, update, toggle and delete

RouteMethodScopeReturns
/command-schedules/{scheduleId}GET:readThe schedule, or 404
/command-schedules/{scheduleId}PATCH:write200 with the updated schedule
/command-schedules/{scheduleId}DELETE:write200 with { "deleted": true }
/command-schedules/{scheduleId}/togglePOST:write200 with { "enabled": boolean }

A 404 also covers a schedule that exists but belongs to a different server, so you cannot probe for ids across servers.

Deleting cannot be undone. If you only want it to stop firing, toggle it off instead - the schedule and its history stay.

Run one now

POST /servers/{id}/command-schedules/{scheduleId}/run — scope command_schedules:control

Starts the schedule's command immediately, whatever its cron says. The run happens in the background, so the answer comes back at once with the run's id. Returns 202 Accepted:

{ "status": "started", "runId": "…" }

The outcome is not in this response. Read it from the execution history or the runs list below, where every row of this run carries that run_id, or subscribe to the webhook.

While a run of the same schedule is still going you get 409 Conflict and nothing is sent twice.

This is a real command against a live server, which is why it needs its own scope. A run-now on a stop schedule stops the server.

Execution history

GET /servers/{id}/command-schedules/{scheduleId}/log — scope command_schedules:read

Most recent first, 50 rows per page (?limit= up to 200). For the next page pass ?before= with the executed_at of the last row you have.

FieldMeans
id, schedule_idIdentity
run_idThe run this row belongs to: every step of one run, and its summary, share it. It is the runId a run-now returns
commandThe command as expanded at execution - variables resolved, one row per player on an all-players step
statussuccess, unverified, error or skipped
outputWhat the server replied, where the channel carries a reply
errorWhy it failed
step_indexnull on the schedule-level summary row; 0-19 on per-step rows
executed_atWhen

`command` is the single most useful field here. It is what actually went to the server, not the template stored on the schedule. A message that went out with {players} still in it, or a player name split on a space, is visible immediately.

A multi-step run produces one row per step that fired, plus a summary row with step_index: null carrying the overall status.

An unverified row's output reads "Sent to the server console. This channel carries no reply, so the game did not confirm it." A skipped row's output carries the reason the condition gave.

GET /servers/{id}/command-schedules/{scheduleId}/runs — scope command_schedules:read

The same history grouped by run, newest run first, 20 runs per page (?limit= up to 50). Each run has its runId, overall status, startedAt, finishedAt, the summary row and its steps. For the next page pass ?cursor= with the nextCursor of the page you have.

Log rows are kept for 90 days.

Variables in text parameters

Any text value in commandParams may contain a variable. They are resolved on our side, per step, at the moment that step runs - so a value read by step 1 and a value read by step 5 twenty minutes later are both current.

TokenBecomes
{players}The number of players online
{playerNames}The online players, comma separated
{server}The server name
{time} / {date}The current time and date, in the schedule's timezone
{nextRun}When the schedule next fires
{randList:a,b,c}One of the options at random
{randNum:1:100}A whole number in the range
{ "commandType": "say",
"commandParams": { "message": "Restart at {nextRun} - {players} online, save your progress" } }

If the roster cannot be read at that moment, {players} resolves to ? and {playerNames} to an empty string, rather than reporting zero players online because a socket blinked.

Full reference: Command Manager Variables.

Webhooks

Every execution - scheduled or run-now - fires command_schedule.executed. Subscribe to it and you are told the outcome of every run without polling the log.

The payload carries the schedule id, the command, the status, output, error, executed_at, the run_id and the number of steps.

A run that ends in a status the schedule's alert settings name (for example error, or skipped) also fires command_schedule.alert, once per run, and emails the person who created the schedule.

See Webhooks for registering an endpoint and verifying signatures.

Limits

LimitValue
Minimum interval between firings5 minutes
Schedules per server50
Steps per schedule20
Wait before a step3600 seconds
Custom command string500 characters
Schedule name100 characters
Cron expression120 characters
Execution history returned50 entries

An interval shorter than the minimum returns 400 with "Minimum schedule interval is 5 minutes".

Errors

Same shape as the rest of the Customer API - branch on code, never on message.

StatusWhen
400Invalid cron, unknown command type, missing required parameter, a value outside a documented range, the 50-schedule cap, or a minimum-interval violation
401Missing or invalid API key
403The key lacks the scope, or the server is not yours
404Schedule or server not found
409The server is suspended, so writes are blocked
429Rate limit exceeded - read Retry-After

A 400 from POST is also how you validate a cron expression with an API key, since the preview route needs a panel session.

The same schedules, elsewhere

A schedule created through the API is the same object the panel edits. There is no separate API-only schedule.