
Customer API Reference
The GameGrid Customer API - authentication, errors, rate limits, pagination, idempotency and streaming, plus every endpoint group and what it covers.
What the Customer API is for
The Customer API lets you build your own tools to manage your own game servers. Anything you can do to a server in the control panel - start it, change its settings, read its console, take a backup, install a mod - you can do from a script instead.
Build a Discord bot that restarts your server, a dashboard on a wall-mounted tablet, or a nightly job that backs up and reports. Everything lives under one address: https://wdcgamegrid.com/api/public/v1.
The API is RESTful, uses standard HTTP methods, returns JSON, and is versioned. Breaking changes go to /v2; new fields and new endpoints are not breaking.
`GET /openapi.json` is the reference that is never out of date. It is generated from the live routes, so it describes exactly what the server you are talking to can do. Point Postman, Insomnia or an SDK generator straight at it rather than transcribing this page.
Your first request
Put the key in an Authorization header and call any endpoint:
curl -H "Authorization: Bearer gsk_YOUR_KEY_HERE" \
https://wdcgamegrid.com/api/public/v1/serversYou get back a paginated list of your servers. The id field on each one is what every server-specific endpoint needs - save it.
A five-minute first script:
1. GET /account/keys/current - what your key can do. No scope required, so this works even if the scopes are wrong.
2. GET /account - who you are.
3. GET /servers - your servers, and their ids.
4. GET /servers/{id}/capabilities - what this particular server supports. Read this before calling anything game-specific.
5. GET /servers/{id}/realtime-stats - whether it is up, and how busy.
6. GET /servers/{id}/connection - what your players should actually type to join.
Steps 1 and 4 answer most "why did I get a 403 or a 501" questions before you have to ask. Get into the habit of reading capabilities first and calling only what they report true.
Authentication
Every request outside /meta/* and /openapi.json carries a bearer token holding an API key:
Authorization: Bearer gsk_XXXXXXXX_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXA key is 56 characters: the prefix gsk_, an 8-character key id, an underscore, and a 43-character secret. The full key is shown exactly once, when you create it in the panel. The API never returns a secret again - GET /account/keys returns metadata only, and the 8-character key id is the safe identifier to log or display.
Keys are created, rotated and revoked in the panel, not through this API. There is deliberately no keys:write scope, so a leaked key cannot mint more keys. The one exception is keys:revoke, which lets a key switch itself off.
Cookies are ignored on this prefix. A browser session signed in to the panel gets a 401 here; the two authentication systems never overlap, and the API cannot be called cross-origin from a browser at all. Without a valid bearer token you get a 401, every time.
All authentication failures return the same 401 invalid_credentials - missing, malformed, unknown and wrong-secret keys are indistinguishable on purpose. The two exceptions are an expired key and a rotated key past its grace window, which return 401 token_expired.
See API Key Management for creating, scoping, rotating and revoking keys.
Finding a server, and telling people how to join it
GET /servers takes two filters so you do not have to pull every page and match in your own code:
| Query | Meaning |
|---|---|
?name= | Case-insensitive substring of the server name. % and _ are matched literally, so searching for my_server finds my_server and not my-server. |
?state= | Exact state, case-insensitive - running, stopped, and so on. A state we do not recognise returns an empty page rather than an error. |
Both combine with ?cursor= and ?limit=, and has_more and next_cursor describe the filtered list. Note that ?name= is charged at the search rate-limit tier, which is lower than read - use it to find a server once, not as a polling loop.
Every row also carries game: {slug, blueprint_version}, the same object GET /servers/{id} returns, so you do not need a call per server to find out which of yours are Valheim.
GET /servers/{id} also returns query_port: the port server browsers, status sites and query libraries ask for the name and player count. It is not always the game port plus one - each game declares its own - so read it rather than computing it. It is null for a game that has no separate query port.
The address your players type
GET /servers/{id}/connection is the one endpoint a status bot or a "join here" page needs:
{
"server_id": "...",
"state": "running",
"host": "203.0.113.9",
"game_port": 28000,
"public_address": "203.0.113.9:28000",
"join_code": null,
"join_code_known": true,
"connection_method": "direct",
"address_accepts_connections": true
}Read connection_method before you show anything:
- `direct` -
public_addressis what players type.address_accepts_connectionsis true. - `join_code` - the server is on a crossplay backend and nothing is listening on its game port. The six-digit
join_codeis the way in, andaddress_accepts_connectionsis false. Every Valheim server ordered with our defaults is in this state, so a bot that printshost:portunconditionally is printing an address that refuses connections. - `unknown` - we could not reach the server host, so we do not know which of the two it is.
join_code_knownis false andaddress_accepts_connectionsis null. Show what you have and say it is unconfirmed; do not fall back tohost:port, because that is exactly the case we could not rule out.
hostis deliberately separate frompublic_address. Usehostwhen you need the hostname on its own (an FTP client, a Steam query library) - do not splitpublic_addresson:, because on a crossplay server it carries the join code as well.
Capabilities - what each server can do
GET /servers/{id}/capabilities reports what this server actually supports, in eleven groups: lifecycle, console, logs, files, settings, players, backups, mods, maintenance, economy and map.
Games differ. A route can exist on the API and still be unsupported on your particular server - that returns 501 capability_unsupported, which is not an error in your code. Read capabilities first and call only what they report true, and you will not write handling for a 501 you could have avoided.
The economy group
Worth its own paragraph, because one of its flags has no other way of being discovered.
| Flag | Means |
|---|---|
shop | Wallets, shop and purchases are available on this server. |
itemCatalogue | The item list can be read from the game. |
linking | A Discord account can be linked to an in-game character. |
automaticDelivery | A purchase reaches the player without a human. False means the sale is recorded as a claim for a server admin to fulfil. |
chatBridge | In-game chat can be mirrored to Discord and back. |
automaticDelivery: falseis not an error state and does not fail a purchase.POST /economy/purchasesucceeds, the wallet is debited and the transaction is recorded — and no item arrives, because a person delivers it. No game the platform hosts today reports it false once its economy is available, but it is the flag that tells you, so read it rather than assuming. If you are building a shop bot, read this flag and tell the buyer what to expect.
Every flag in the group follows a switch the customer can flip: on a game whose economy is carried by an add-on, all five are false until the add-on is installed. Windrose is the case today — its economy is provided by Windrose Plus, and switching that on is what turns the whole group true. Read capabilities per request rather than caching them for the life of a session.
Windrose does deliver automatically, so
automaticDeliveryis true there once Windrose Plus (which GameGrid installs on every Windrose server) has loaded. What differs on Windrose is *where* the item lands: on the ground at the player’s feet rather than in a backpack, because the character lives on the player’s own machine. The capability flags do not report that — see Economy Overview.
Errors - always the same shape
Every error, on every route, comes back like this:
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the required scope.",
"request_id": "req_...",
"details": { }
}
}Branch on `code`, never on `message`. Codes are stable and changing one is a breaking change. Messages may be reworded at any time. Quote
request_idin a support ticket and we can find the exact request.
| Code | HTTP | When |
|---|---|---|
invalid_request | 400 | The body, query or path does not match the schema. Also a malformed Idempotency-Key, and a file route aimed at a file or folder GameGrid manages for you (details.rule is platform_managed). |
invalid_cursor | 400 | The pagination cursor is not one we issued. |
invalid_credentials | 401 | Missing, malformed, unknown or wrong-secret key. |
token_expired | 401 | The key expired, or a rotation grace period ended. |
insufficient_scope | 403 | The key was never granted this scope. |
permission_revoked | 403 | The scope is on the key, but its owner lost the underlying account permission. |
api_key_server_restricted | 403 | The key is limited to particular servers, and this route belongs to the whole account: the audit trail, the key list, entitlements, the billing summary and overview, support tickets, server bundles and webhooks. Use a key without server restrictions. |
server_deactivated | 403 | The plan expired. Renew to use the server again - your files stay reachable in the file manager and over FTP. |
plan_over_slot_limit | 403 | The plan renewed with fewer slots than it has servers, so every server on it is stopped. Delete servers down to the slot count, or add slots on the Manage Plan page; nothing is deleted automatically, and files stay reachable in the file manager and over FTP. |
self_service_off | 403 | The server's owner has switched personal API tokens (or Discord /my) off. A personal token stops working when you get this. |
section_not_shared | 403 | The server's owner does not share that part of your stats. |
metrics_disabled | 404 | Metrics is switched off on GameGrid for now. |
metrics_public_surfaces_off | 404 | Public Metrics pages (such as public leaderboards) are switched off on GameGrid for now. |
legal_reacceptance_required | 403 | The Terms of Service or Privacy Policy changed and must be accepted again in the customer panel before the API can make changes. Reads keep working. |
server_not_found | 404 | The server does not exist, belongs to someone else, or is outside this key's server restrictions. One response for all three. |
resource_not_found | 404 | A non-server resource was not found. |
route_not_found | 404 | No such route - or the capability is unavailable on your account. If a documented route returns this, contact support. |
state_conflict | 409 | The action conflicts with the current state, e.g. starting a running server, or installing or removing a mod while a Vault save is being loaded onto the server. For start, stop, restart and kill, details.reason says which: server_already_running (a start of a running server), server_state_not_startable (a start while it is installing, waiting for capacity or otherwise not at rest), server_not_running (a stop or kill of a server that is already stopped, failed or needs attention), server_state_not_stoppable, server_state_not_restartable, no_host (a start or restart of a server that has not been placed on a host yet), or lifecycle_action_in_progress (another start, stop or restart is still running). For a file rename, copy or move, destination_exists says the destination is already there and nothing was changed; send "overwrite": true to replace it. |
server_in_migration | 409 | The server is moving to a new host and cannot be controlled until it lands. |
mod_job_cancel_too_late | 409 | You asked to cancel a mod job (POST /servers/{id}/mods/jobs/{jobId}/cancel) after it started writing to the server. It can no longer be cancelled: it finishes, or fails and says what it left behind. |
mod_job_finished | 409 | You asked to cancel a mod job that has already finished (succeeded, failed or been cancelled), so there is nothing to cancel. |
job_not_cancellable | 409 | This kind of job cannot be cancelled once it has been queued - at present none can, so every POST /jobs/{jobId}/cancel answers this. |
idempotency_conflict | 409 | The same Idempotency-Key was already used with a different body or query string. |
idempotency_key_in_progress | 409 | A request with that key is still running. |
webhook_limit_reached | 409 | Your account already has 100 webhooks, the most allowed. Delete one you no longer use, then register the new one. |
payload_too_large | 413 | Over the 1 MB body limit. |
unsupported_media_type | 415 | Wrong Content-Type for this endpoint. |
validation_failed | 422 | Syntactically valid but semantically wrong - a port out of range, an unknown webhook event, a rejected webhook URL. |
scope_exceeds_permissions | 422 | You asked for scopes you do not hold (key creation). |
confirmation_required | 422 | A destructive action without a matching confirm value. |
rate_limited | 429 | Too many requests. Read Retry-After. Opening tickets (POST /tickets) has its own limit, 5 per person per hour and 20 per account per 24 hours (a key counts as the person who made it): that answer carries Retry-After too (the seconds until you can open another), says which limit in message, and details.scope is user or organization. Replying to a ticket (POST /tickets/{ticketId}/messages) is limited too, to 20 replies per ticket per hour: that answer has details.scope ticket and carries Retry-After as well. |
stream_limit_reached | 429 | Your account already has 50 log streams open at once. Close one, then mint a new ticket. |
internal_error | 500 | Something broke on our side. Quote the request_id. |
capability_unsupported | 501 | The route exists, but this game or server cannot do it. |
upstream_unavailable | 502 | The machine your server runs on could not be reached. |
service_unavailable | 503 | Temporarily unavailable. |
encryption_key_unavailable | 503 | The platform could not save a secret (a webhook signing secret, or a new FTP password) because the key it protects secrets with is not available. Nothing was changed. A temporary platform fault, not a problem with your request - retry shortly. |
suspension_check_unavailable | 503 | Your server's subscription status could not be checked, so the action was refused. A temporary platform fault, not a problem with your plan - retry shortly. |
upstream_timeout | 504 | The machine your server runs on did not answer in time. |
The Vault's codes
The Vault's routes (the vault group below) answer these as well, besides the codes above. The Vault's own codes are the same words the panel uses, so a client and the panel branch on the same code; the numbers a code carries are in details.
| Code | HTTP | When |
|---|---|---|
vault_part_unknown | 400 | A load named a part the save does not hold or the platform cannot apply. |
api_key_server_restricted | 403 | The key is limited to particular servers, so it cannot use the routes that belong to the whole account (the list, one save, its download, its deletion, a load's progress). A save belongs to the account, not to a server. |
insufficient_permission | 403 | The key has the scope, but the action needs one more permission of its owner, checked live: downloading a save needs the account owner or an administrator; saving a server also needs the right to change its settings, and loading a save the right to control it, exactly as in the panel. details.required_permission names it. |
vault_disabled | 404 | The Vault is switched off on this platform: every Vault route answers this. |
vault_full | 409 | The save would not fit in your Vault, or your Vault has no free space at all. details carries neededBytes, freeBytes and quotaBytes. |
vault_too_large | 409 | The server's files or the backup are over 2 GB, the most one save can hold. |
vault_game_mismatch | 409 | A load named a save of another game than the server's (exact game only; each Minecraft type is its own game). The message names both games. |
vault_server_never_started | 409 | Save to Vault was asked for a server that has never run: it has no world yet, so nothing is started. |
vault_no_world | 409 | A load named a save that holds no world files, so loading it would leave the server without a world; nothing was changed. A save that would hold no world fails with the same code. |
vault_mod_job_running | 409 | A load was asked while a mod install, update or removal of the server is queued or running. Try again when it has finished. |
vault_build_mismatch | 409 | A load was asked without "acceptBuildWarning": true for a server that runs an older game build than the save was made on (7 Days to Die: any other build). The message names both builds; GET /servers/{serverId}/vault/compatible names it per save as buildWarning. |
vault_item_loading | 409 | You tried to delete a save while it is being loaded onto a server. |
vault_item_not_ready | 409 | A download or a load named a save that is still being made, or failed. |
vault_backup_not_verified | 409 | Keep in Vault named a backup that is not finished and verified. |
vault_uploaded_backup | 409 | Keep in Vault named a backup you uploaded: the Vault only holds worlds saved from GameGrid servers. |
vault_server_busy | 409 | A save was asked while a Vault load runs on the server, or a load while another load, a backup restore, a start, stop or restart, a wipe, a savegame delete, a reinstall, a repair or a game update is in progress. A load that could not begin for one of those reasons, or because the server was being rebuilt after a host failure or was still being set up, ends with this code too. |
vault_server_running | 409 | A load (without whenReady) was asked for a server that is running, starting, restarting or stopping. Stop it first. |
vault_server_migrating | 409 | A save or a load was asked while the server is moving to another machine. |
vault_backup_already_running | 409 | Save to Vault was asked while a backup of the server is running. |
vault_server_backup_quota_full | 409 | Save to Vault takes a fresh manual backup first, and the server's backup storage has no room for it, even after its oldest automatic backups are removed. |
vault_saves_busy | 409 | Two saves of your account are already being made (Save to Vault or Keep in Vault). Nothing is started; try again when one has finished. |
vault_backup_already_saving | 409 | Keep in Vault named a backup that a save still being made is already copying. |
GET /meta/error-codes returns this catalogue as JSON and needs no key.
Rate limits
Every route sits in one of nine tiers. All windows are 60 seconds.
| Tier | Requests/min | Covers |
|---|---|---|
read | 300 | Every ordinary read, including the SSE streams themselves |
poll | 600 | realtime-stats, config-event status and world map tiles (map/world/tile) - meant to be polled, or fetched many at a time |
search | 60 | Server search by name; mod marketplace search |
write | 120 | Ordinary writes |
control | 30 | Lifecycle, maintenance, restore, rollback, config-event start/end |
download | 20 | File and log downloads, metrics export, Metrics CSV datasets |
upload | 10 | File uploads |
stream | 60 | Minting a stream ticket |
self | 60 | A player's personal token (gpt_) reading /me/*: per token, and 600 a minute for all player tokens of one server |
Two extra caps apply to API keys. Per-server control: one key can send at most 10 control commands to the *same* server per minute. Organization multiplier: all of an organization's traffic in a tier is capped at 2× the per-key limit.
Every response carries the current state:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The limit for this tier |
X-RateLimit-Remaining | What is left, already counting this request |
X-RateLimit-Reset | Seconds remaining, not a Unix timestamp |
X-RateLimit-Tier | Which of the nine tiers applied |
X-RateLimit-Scope | Which limit was checked: key, key+server, org, or none |
Retry-After | On a 429 only: seconds to wait |
Rejected requests still count against your budget, so backing off properly matters. Read
Retry-Afterrather than retrying immediately.
GET /meta/rate-limits returns the tiers as JSON and needs no key.
Pagination
List endpoints use cursor pagination:
{ "data": [ ], "pagination": { "has_more": true, "next_cursor": "eyJ0Ijo..." } }Pass the cursor back as ?cursor=... to get the next page. Keep going while has_more is true; next_cursor is null once it is false.
| Setting | Value |
|---|---|
| Default page size | 50 |
| Maximum page size | 100 |
| Over the maximum | Clamped to 100 with a normal 200 - not an error |
A malformed, zero or negative limit | 422 validation_failed |
| A cursor we did not issue | 400 invalid_cursor |
GET /account/audit is the exception: it defaults to 100 and allows up to 500, over a fixed 24-hour window.
Some cursor lists have their own page sizes, and they are newest first. Settings history and the event list (config-events) default to 100 and allow at most 100. Economy links default to 100 and allow at most 500. Economy transactions and game events (economy/events) default to 50 and allow at most 200; on those three economy lists a limit that is not a whole number of at least 1 falls back to the default instead of answering 422. The shop and the catalogue page by number instead (page and limit) and answer pagination with total, page, limit and has_more. GET /servers/{id}/economy/links?discord_user_id=... returns only that Discord user's links, and has_more and next_cursor describe the filtered list.
Not every list is paginated, and the shape varies. Endpoints that return a complete set - backups, files, mods, current players, bundles, mod search - include
paginationwithhas_more: falseand no cursor. Two endpoints return no cursor at all:GET /servers/{id}/logsreturnspagination: null, andGET /servers/{id}/console/historyomits the field entirely. Test for the field's presence rather than assuming it is there, and treat the cursor as opaque - do not build one by hand.Current players can be a recent list, marked stale. If
GET /servers/{id}/playerscannot read the live list (the server is restarting, the game is still starting up, or its machine did not answer), it returns the last list it read live in the past 10 minutes with three extra fields:stale: true,as_of(when that list was read) andstale_reason(server_not_running,game_not_answeringorhost_unreachable). A live answer has none of them. With nothing that recent, the answer is502 upstream_unavailableas before. A bot that shows player counts can show the stale list with its age, or wait for the next live answer.
Idempotency
Every POST, PUT, PATCH and DELETE accepts an optional Idempotency-Key header. Send a UUID you generate; anything that is not a valid UUID is rejected with 400 invalid_request.
It is optional everywhere but one route. Send it on anything you would not want done twice: creating a backup, the maintenance actions, mod installs, file and backup uploads. Without the header, a retry does the work again. Starting a new stats season (
POST /servers/{id}/metrics/seasons) requires it, so a retried request can never start two seasons.
| Situation | Result |
|---|---|
| Same key, same body and query string | The original response is replayed, with Idempotency-Replayed: true |
| Same key, different body or query string | 409 idempotency_conflict |
| Same key, first request still running | 409 idempotency_key_in_progress |
A stored response can be replayed for 24 hours. Only successful responses are stored - if a request failed, a genuine retry runs again rather than replaying the failure. Bodies are compared with key order ignored, so re-serialising JSON does not break a replay. A key is scoped to one route, so the same UUID cannot accidentally deduplicate a call against a different server, and the query string counts: DELETE .../files?path=a.txt and ?path=b.txt are two different requests. Asking for a stream ticket is never replayed: retrying it gives you a fresh ticket.
Destructive actions need typed confirmation
Four actions require you to type something before they run: reinstall, wipe, delete-savegames and update. (verify does not - repairing files is not destructive.)
Send the server's exact current name in a confirm field:
{ "confirm": "Community Survival" }The comparison is exact - no trimming, no case folding. Get it wrong or leave it out and you get 422 confirmation_required, whose details name the action, the field, and the exact value expected:
{ "error": { "code": "confirmation_required", "details": {
"action": "wipe", "expected_field": "confirm",
"expected_value": "Community Survival",
"hint": "Send the server's exact current name in the `confirm` field." } } }This is a deliberate speed bump that a script cannot pass by accident. Ownership is checked before the confirmation, so seeing the expected value in the error means you already had access to that server.
Live console and log streaming
Streaming takes two calls, because a browser EventSource cannot send an Authorization header.
1. Mint a ticket with your normal bearer token: POST /servers/{id}/console/stream-tickets (scope console:read) or POST /servers/{id}/logs/stream-tickets (scope logs:read). You get back a stream_url.
2. Open that stream_url with EventSource.
Tickets last 60 seconds and are single-use. Mint one per connection and do not cache them. A ticket is bound to one key, one server and one kind - a logs ticket will not open a console stream, and trying burns the ticket anyway. A ticket carries exactly the minting key's scopes, no more, so a stream leaves out players' chat lines for a key that may not read chat, as the history and tail do.
| Console stream event | Payload |
|---|---|
connected | { server_id, status } - sent immediately |
history | The recent buffer, as an array of lines |
console | One line as it arrives |
heartbeat | { ts } every 30 seconds |
error | { error } - the stream then ends |
| Log stream event | Payload |
| --- | --- |
connected | { server_id, status } |
log | { text, timestamp } - the first poll sends the current tail |
gap | { skipped } - more lines arrived than one poll window holds, so some were lost. We say so rather than show you a partial, misordered view. |
heartbeat | { ts } every 25 seconds |
error | { error } - stream_lifetime_reached or slow_consumer; the stream then ends |
Console streams have no fixed lifetime and no concurrency cap; log streams have both. A console stream stays open until you disconnect, the source ends, or your client stops reading: a console client that falls about 2 MB behind gets
error{ error: "slow_consumer" }and should reconnect withLast-Event-ID. A log stream ends after 60 minutes witherror{ error: "stream_lifetime_reached" }. A log client that falls behind getsgap; one that stays about 2 MB behind for 30 seconds getserror{ error: "slow_consumer" }. Since 8 October 2026 your account can hold up to 50 log streams open at once, and the next ticket or connection is refused withstream_limit_reached(429). Handleerrorand ordinary disconnects by minting a fresh ticket and reopening - because tickets are single-use, every reconnect needs a new one.
Field conventions
Use snake_case field names. Some answers also carry an older camelCase name beside the snake_case one, with the same value, so existing integrations keep working: progressPercent (read progress_percent), jobId (read job_id), and the camelCase fields of DayZ events, Discord server links, leaderboards and player access (read the snake_case twin). Request bodies on those routes, and on the economy routes, accept either spelling. The older names are marked deprecated in the OpenAPI document. One answer is still camelCase only: the mod install result's own fields (hashCheck, notice, warning). All timestamps are ISO 8601 UTC with milliseconds and a Z suffix, e.g. 2026-08-16T02:56:02.261Z. Durations end in _seconds; byte counts end in _bytes; percentages end in _pct.
Booleans are real booleans, never the strings "true" / "false". Some large integers arrive as JSON strings to avoid precision loss - parse them rather than assuming typeof === "number".
null means "no value", never zero. This matters most for player counts and metrics on a stopped server.
A player's key or playerKey (Metrics and DayZ routes) is an opaque handle, k- followed by 22 characters. It is the same every time for the same player, so you can store it, but it is private to your account: it is not a Steam or game ID, another account sees the same player under a different key, and a key from another account matches nothing. An older p- followed by 16 characters, saved before keys were made private, is still accepted.
Singular resources are returned bare. Collections are wrapped in data - see Pagination above for when pagination is present and when it is not.
What the API deliberately does not do
Some things are panel-only, on purpose. These are not missing - they were left out for a reason:
- Creating or deleting servers. Both change what you pay. That belongs somewhere a human clicks a button and sees the price.
- Spending money. Nothing in this API can trigger a payment, buy a slot or change a card.
- Creating API keys. No scope lets a key create another key, so a leaked key cannot make more.
- Moving servers between paid slots. Commercial state, same reasoning as creating servers.
You can still read your billing summary and the plan list. You just cannot spend from a script.
Discovery endpoints - no key required
These need no authentication at all. Use them to build clients and to check your assumptions against a live server:
| Endpoint | Returns |
|---|---|
GET /meta/scopes | Every scope and what each grants |
GET /meta/error-codes | The error catalogue |
GET /meta/rate-limits | The nine tiers, the per-server control limit and the organization multiplier |
GET /meta/games | Supported games |
GET /meta/webhook-events | Every event a webhook can subscribe to, plus the test ping |
GET /openapi.json | The full machine-readable specification |
Recent changes
- 2026-10-26: responses match the published document. Five of these change what you receive:
GET /servers/{id}/filesentries are{ name, type, size, modified_at };truncated: true(andpagination.has_more) means the folder has more than the 2000 entries listed.GET /servers/{id}/player-listsentries carryname(the same value aslabel, which stays).- The billing-summary counts and backup
size_bytesare numbers, not strings. - Battle Royale
round_dayis a date (2026-09-24), not a timestamp. - The DayZ held prizes
totalis everything owed, not just the 100 lines listed.
Also: dayz/sales?limit= must be a whole number (400 otherwise). A settings save changes the server's JAR only after the settings are written, and a JAR change that then fails shows in not_applied_keys and warnings. A kit-claim Idempotency-Key reused for a different player is 409 state_conflict (idempotency_key_reused). Plans carry billing_interval. The specification now documents restart_required and new_world_only as lists of keys (check their length, not their truth), the real player-row fields, and the 201 that the console command, settings rollback and maintenance actions answer.
- 2026-10-26: more lists page. The event list (
GET /servers/{id}/config-events) now answers{ data, pagination }instead of a bare list, so readdata. Links, transactions, game events and settings history page withpagination.next_cursor(pass it back as?cursor=), and links can be filtered with?discord_user_id=. The shop and the catalogue now sayhas_more. - Guild links, leaderboards, player access and the economy take snake_case, and a leaderboard can be sent back as you read it (2026-10-26).
PUT /integrations/discord/guilds/{guildId}, the leaderboard routes,PUT .../metrics/player-accessand every economy route that takes a body now take snake_case names:guild_name,board_key,public_command,live_positions,discord_user_id,item_asset_nameand so on, inside a kit's items too. The camelCase names still work. The answers of the guild link, the leaderboards, player access and the economy configuration carry the snake_case names beside the camelCase ones; none is removed or renamed. You canPUTa leaderboard back exactly as aGETgave it (the fields only an answer has are ignored); aboardIdin the body that is not the one in the path is400 invalid_request. Sending the same field in both spellings with different values is now400 invalid_request(details.reasonisconflicting_spellings) and changes nothing. Before,PUT .../economy/configandPUT .../economy/chat/bridgequietly kept one of the two, andPUT .../economy/shop/{itemId}failed with a500. The same value in both spellings is fine. - DayZ events take snake_case, and an event can be sent back as you read it (2026-10-26).
POSTandPUT .../dayz/game-eventstakeduration_minutes,selects.hot_zones,rewards.kill_bonus.per_player_capand so on, with the camelCase names still working. You canPUTback an event exactly as aGETgave it. A temporary area takesvisible_to(admin,membersorpublic). The timed steps are edited in the panel: leaveselects.timelineout and they are kept; aPUTno longer deletes them. - Idempotency counts the query string. Reusing an
Idempotency-Keyon the same route with a different query (?path=a.txt, then?path=b.txt) is now409 idempotency_conflictinstead of a replay of the first answer. A trailing slash no longer makes a second route. - Stream tickets are never replayed. Retrying
POST .../logs/stream-ticketsorPOST .../console/stream-ticketswith the same key gives you a fresh ticket. They also work on a deactivated server now, like every other way of reading its logs. - List rows no longer carry `created_at_cursor`. It was an internal field. Page with
pagination.next_cursor. - A log file's `size` is a number or `null`, never
false. - Mod refusals keep their meaning. Turning a mod on or off, or installing one, answers the actual reason (for example "re-pack it as a .zip") with a
400,404or422, instead of a bare404or a502. - Four more error codes are listed in
GET /meta/error-codes:self_service_off,section_not_shared,metrics_disabled,metrics_public_surfaces_off. - New snake_case fields:
progress_percent(migration status) andjob_id(mod install), beside the older camelCase names. - Renaming, copying or moving onto something that already exists no longer replaces it.
POST /servers/{id}/files/rename,.../copyand.../movenow answer409 state_conflictwithdetails.reasondestination_existsand change nothing when the destination is already there. Send"overwrite": truein the body (it is optional,falseby default) to replace it. Before, a move or rename replaced an existing file and a copy overwrote a file or merged into a folder without asking. - The Valheim map leaderboard says how far it pages.
totalis the whole board;pageable_totalis how many rows the cursor can reach andtruncatedis true when that is fewer. A cursor that is not one we handed out is400 invalid_cursorinstead of quietly starting again at page 1.
Endpoint groups at a glance
The API is organised into 31 groups totalling 307 operations. Counts are operations, not paths - one path like /servers/{id}/backups/{backupId} carries a GET, a PATCH and a DELETE.
| Group | Ops | What it covers |
|---|---|---|
account | 10 | Account, organization, entitlements, roles, keys, audit trail |
billing | 2 | Billing overview and plan list |
bundles | 3 | Server bundles and slot usage |
servers | 7 | List (filter by ?name= and ?state=), read, rename, status, connection address, real-time stats, migration status |
lifecycle | 4 | Start, stop, restart, kill |
lifecycle-history | 3 | State history and auto-restart |
metrics | 58 | Live metrics, export, resource limits, diagnostics; and Metrics history: summary, time series, busiest hours, uptime, heat, places, stats seasons, the player ranking and search, player profiles, sessions, travel and places, CSV datasets (metrics/datasets/{dataset}/export), and the same across all your servers (/metrics/summary, /metrics/timeseries, /metrics/players/{key}); activity, events and economy history, chat, the kill feed and combat, chart images (metrics/charts/{chart}), On Watch, alerts, possible alternative accounts and owner notes; leaderboards (/servers/{id}/leaderboards: read, set up, post); and the Discord bot's /my (/servers/{id}/self). Player data needs player_metrics:read; identifiers and seasons need metrics:admin; chat needs player_metrics:chat, alternative accounts player_metrics:alts, leaderboard set-up leaderboards:write and /my the players_self scopes |
jobs | 3 | List jobs, read one job's status, and cancel (every cancel is refused today with job_not_cancellable). A job shows only the payload fields meant for you, and backup and restore jobs from before this change are not listed |
console | 5 | History, command, clear, stream ticket, stream. Players' chat lines are left out unless your key may read chat: it needs player_metrics:chat and must have been created by the account owner or an administrator (left out of the history, the stream and a command's reply) |
logs | 5 | List, tail, download, stream ticket, stream. Players' chat lines are left out unless your key may read chat: it needs player_metrics:chat and must have been created by the account owner or an administrator (left out of the tail, a downloaded log and the stream; a kept line keeps its own line number). Passwords, tokens and other secrets in a log line show as [redacted], player IP addresses as [ip-hidden] and email addresses as [email]. A recognised key or token with no label in front of it (a GameGrid API key, a Discord bot token or the token in a Discord webhook link, a Stripe, GitHub, Google, AWS, OpenAI, Slack or Tailscale key) shows as [api-key], and a private key as [private-key] |
files | 15 | The full file manager, plus FTP and SFTP details. Files GameGrid manages for you (launch scripts and other executables, the Windrose Plus configuration files and the like) are left out of listings and refused, the same as in the file manager. Reading a configuration file with passwords in it (server.properties, serverDZ.cfg, ...) shows the real values only to a key that has the settings:write scope and whose owner can change the server's settings; any other key gets them as •••••••• with secrets_masked: true, and reading an archive is refused with insufficient_scope. GameGrid's own 7 Days to Die key in serveradmin.xml is masked for every key. Reading a log file leaves out players' chat lines unless your key may read chat (player_metrics:chat, on a key created by the account owner or an administrator) |
settings | 12 | Schema, values (all, or one key), validate, history, rollback, drift, detect, worlds, and one stored password of a table setting (POST settings/{key}/table-secret). A table setting (a list of named entries, such as Enshrouded's user groups) reads back with every password shown as ••••••••; write the whole list back and an entry whose password is still •••••••• keeps the password it had, matched by its name |
players | 5 | Player counts and player lists |
discovery | 2 | Server capabilities and the player-list catalogue |
map | 10 | Live map: capabilities, players, entities, fog, leaderboard, share link, land claims, and for a game with a fixed world map (Enshrouded) the map itself: map/world, its image tiles (map/world/tile) and its places (map/world/layers) |
backups | 8 | List, read one, quota, restore-job status, create, label, delete, restore (no upload or download: those are on the panel's Backups page) |
backup-schedule | 2 | Read and set the backup schedule |
vault | 9 | Your account's Vault: list your saves, read one, download it, delete it and follow a load (/vault, /vault/items/{itemId}, .../download, /vault/loads/{loadId}); per server, the saves it can load (/servers/{serverId}/vault/compatible), Save to Vault (POST .../vault/save, with an Idempotency-Key), Keep in Vault from a backup (POST .../backups/{backupId}/vault) and Load from Vault (POST .../vault/load). Scopes vault:read and vault:write; see Your Vault |
mods | 13 | Installed mods, marketplace search, install, remove, toggle, reorder, and the mod jobs: list them, read one, and cancel one before it writes to the server (/servers/{id}/mods/jobs, .../jobs/{jobId}, .../jobs/{jobId}/cancel). The install answer now carries a jobId and removing a mod answers with a job_id; removing a mod is refused with state_conflict while a Vault save is being loaded onto the server |
maintenance | 9 | Verify, reinstall, wipe, delete savegames, update, history |
config-events | 21 | Config presets and scheduled events - a 2x XP weekend, say - plus the ready-made presets for your game (config-presets/library), where players are warned before an event restarts the server (config-events/warning-channels) and the warnings already sent (config-events/restart-notices). An event's log leaves out players' chat lines unless your key may read chat (player_metrics:chat, on a key created by the account owner or an administrator) |
command-schedules | 10 | The command catalog and scheduled console commands: create, update, toggle, run now, history, runs. The stored output of a command leaves out players' chat lines unless your key may read chat (player_metrics:chat, on a key created by the account owner or an administrator) |
economy | 34 | Wallets, shop, the item catalogue, purchases, transactions, links, chat bridge, kits |
windrose-plus | 7 | Windrose Plus status, config, enable and disable, the RCON password, and turning UE4SS back on after GameGrid paused it (loader/unpark) |
dayz | 22 | DayZ servers only: players, kill feed, leaderboard, player profiles, events, chat, broadcast, sales, presets, in-game events with their runs and held prizes, and the Battle Royale round and ring map link. Every route is in DayZ in the Customer API and the Discord Bot |
webhooks | 8 | Register, update, test, redeliver, delivery history (a delivery that was held back and replaced by a newer event is listed with coalesced: true; see Webhook Configuration) |
player-self-service | 8 | A player's own stats with a personal player token instead of an API key (/me/*: profile, sessions, travel, places, economy, events, deaths, one leaderboard) |
tickets | 4 | Read and open support tickets |
integrations | 2 | Link and unlink a Discord guild |
meta | 5 | Unauthenticated discovery |
openapi | 1 | The specification document |
For the full per-endpoint reference with request examples, response shapes, scopes and rate-limit tiers, use GET /openapi.json. It is generated from the live routes, so it is the one reference that cannot drift.