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-catalogis 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-catalogA 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
| Scope | Grants |
|---|---|
command_schedules:read | List and read schedules, read the command catalog, read execution history |
command_schedules:write | Create, update and delete schedules; toggle them on and off |
command_schedules:control | Run 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.
:controlis the one to think about. A key that only reports on schedules needs:readand nothing else, and cannot then be used to fire astopat 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 } ] }
]
}| Field | Means |
|---|---|
allPlayersSelector | The game's own word for "everyone", or null. "@a" on Minecraft; null on every other game |
commands[].id | What you send as commandType |
commands[].syntax | The template. {braces} are parameter keys |
commands[].warning | Present when the command is destructive or behaves oddly on a schedule. Show it to your user |
params[].playerTarget | true 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:
| Game | Commands | allPlayersSelector | Player-target commands |
|---|---|---|---|
| 7 Days to Die | 46 | null | 10 |
| Windrose | 25 | null | 9 |
| Minecraft | 24 on Vanilla and Fabric, 25 on Forge and NeoForge, 26 on Paper | "@a" | 10 |
| DayZ | 26 | null | 0 |
| Palworld | 11 | null | 4 |
| Valheim | 8 | null | 1 |
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.
| Field | Means |
|---|---|
id, name | Identity |
command | The rendered command string for a single-command schedule |
command_type, command_params | What it was built from |
steps | null on a single-command schedule; an ordered array on a multi-step one |
cron_expression, timezone | When it fires, and in which timezone the cron is read |
enabled | Whether it fires at all |
next_run_at | The next firing, computed server-side |
last_run_at, last_run_status, last_run_output, last_error | The most recent run |
run_when, min_players | The run condition |
notify_on | Which outcomes raise an alert |
overlap_policy, catchup_window_minutes, jitter_seconds, missed_tolerance | Reliability settings |
consecutive_misses, last_run_ms, running_until | Scheduler bookkeeping you can read |
last_run_status
| Value | Means |
|---|---|
success | Sent, and the server replied |
unverified | Sent down a channel that carries no reply. Treat as "sent", not as "worked" |
error | Not delivered, or the server rejected it |
skipped | A run condition turned this occurrence away. Not a failure |
late | More consecutive runs missed than missed_tolerance allows |
unverifiedis normal on Minecraft, whose console returns nothing. It is honesty, not a fault - see Command Manager Troubleshooting.
Step objects
| Field | Means |
|---|---|
commandType, commandParams | What this step runs |
command | The rendered string |
delaySeconds | Seconds to wait *before* this step. Ignored on the first step |
targetAllPlayers | Whether this step expands to every online player |
playerParamKey | Which 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
overlapPolicyis 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:
| Game | Sends | Identifier substituted |
|---|---|---|
| Minecraft | One command | @a - the game resolves "everyone" itself |
| Palworld | One per player | Platform ID (steam_, gdk_, ps5_) |
| 7 Days to Die | One per player | Entity ID - digits, because a name can contain a space |
| Valheim | One per player | Session ID, which changes on every reconnect |
| Windrose | One per player | Player name - no numeric handle exists |
| DayZ | - | No DayZ command has a playerTarget parameter, so there is nothing to expand |
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.
| Field | Type | Default | Notes |
|---|---|---|---|
name | string | โ | Required. 1-100 characters |
cronExpression | string | โ | Required. 5-field cron, up to 120 characters |
timezone | IANA name | UTC | The timezone the cron is read in |
enabled | boolean | true | A disabled schedule stays stored and never fires |
commandType | string | โ | Single-command schedules. Mutually exclusive with steps |
commandParams | object | {} | Values for that command's parameters |
steps | array | โ | 1-20 step objects. Send null on a PATCH to make it single-command again |
steps[].delaySeconds | number | โ | 0-3600. Wait before this step. Ignored on the first |
steps[].targetAllPlayers | boolean | false | Expand this step to every online player |
steps[].playerParamKey | string | first match | Which parameter is the player target |
runWhen | enum | always | always, server_online, players_online |
minPlayers | number or null | null | 1-1000. A floor below which the run is skipped |
notifyOn | array | [] | Up to 3 of failure, skipped, success |
overlapPolicy | enum | skip | skip, queue, allow - when it comes due mid-run |
catchupWindowMinutes | number | 15 | 0-1440. How late a run may still happen |
jitterSeconds | number | 0 | 0-900. Start inside a random window, not on the minute |
missedTolerance | number | 1 | 0-50. Consecutive misses before the schedule is flagged late |
What the reliability fields actually do
| Field | Why it exists |
|---|---|
overlapPolicy | Two overlapping restarts is a corrupted world. skip drops the occurrence; queue waits for the previous run; allow runs both |
catchupWindowMinutes | A 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" |
jitterSeconds | Ten servers that all save at 04:00 hit the same disk in the same second. Spread them |
missedTolerance | One blip should not wake anybody. A schedule that has quietly stopped firing should be impossible to miss |
Updating a schedule that had been flagged
lateclears 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 agsk_key, and is read-only. There is no API-key equivalent yet, so to validate an expression with a key,POSTthe schedule and read the400.
Read, update, toggle and delete
| Route | Method | Scope | Returns |
|---|---|---|---|
/command-schedules/{scheduleId} | GET | :read | The schedule, or 404 |
/command-schedules/{scheduleId} | PATCH | :write | 200 with the updated schedule |
/command-schedules/{scheduleId} | DELETE | :write | 200 with { "deleted": true } |
/command-schedules/{scheduleId}/toggle | POST | :write | 200 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
stopschedule 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.
| Field | Means |
|---|---|
id, schedule_id | Identity |
run_id | The run this row belongs to: every step of one run, and its summary, share it. It is the runId a run-now returns |
command | The command as expanded at execution - variables resolved, one row per player on an all-players step |
status | success, unverified, error or skipped |
output | What the server replied, where the channel carries a reply |
error | Why it failed |
step_index | null on the schedule-level summary row; 0-19 on per-step rows |
executed_at | When |
`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, exactly as you received it: it is an opaque token, so do not read a time out of it or build one yourself. Runs that finish at the same moment page correctly, each shown once.
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.
| Token | Becomes |
|---|---|
{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
| Limit | Value |
|---|---|
| Minimum interval between firings | 5 minutes |
| Schedules per server | 50 |
| Steps per schedule | 20 |
| Wait before a step | 3600 seconds |
| Custom command string | 500 characters |
| Schedule name | 100 characters |
| Cron expression | 120 characters |
| Execution history returned | 50 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.
| Status | When |
|---|---|
400 | Invalid cron, unknown command type, missing required parameter, a value outside a documented range, the 50-schedule cap, or a minimum-interval violation |
401 | Missing or invalid API key |
403 | The key lacks the scope (insufficient_scope), or the server is suspended, so writes are blocked (server_deactivated) |
404 | Schedule or server not found - including a server that is not yours or is outside the key's server restrictions (server_not_found) |
409 | A run of the same schedule is still going |
429 | Rate 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
| Where | Article |
|---|---|
| The panel | Command Manager Guide |
| Discord | Scheduling Commands from Discord |
| Variables reference | Command Manager Variables |
| When something is wrong | Command Manager Troubleshooting |
A schedule created through the API is the same object the panel edits. There is no separate API-only schedule.