Skip to main content
Customer API Reference - visual guide

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/servers

You 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_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

A 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:

QueryMeaning
?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_address is what players type. address_accepts_connections is true.
  • `join_code` - the server is on a crossplay backend and nothing is listening on its game port. The six-digit join_code is the way in, and address_accepts_connections is false. Every Valheim server ordered with our defaults is in this state, so a bot that prints host:port unconditionally 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_known is false and address_accepts_connections is null. Show what you have and say it is unconfirmed; do not fall back to host:port, because that is exactly the case we could not rule out.

host is deliberately separate from public_address. Use host when you need the hostname on its own (an FTP client, a Steam query library) - do not split public_address on :, 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.

FlagMeans
shopWallets, shop and purchases are available on this server.
itemCatalogueThe item list can be read from the game.
linkingA Discord account can be linked to an in-game character.
automaticDeliveryA purchase reaches the player without a human. False means the sale is recorded as a claim for a server admin to fulfil.
chatBridgeIn-game chat can be mirrored to Discord and back.

automaticDelivery: false is not an error state and does not fail a purchase. POST /economy/purchase succeeds, 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 automaticDelivery is 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_id in a support ticket and we can find the exact request.

CodeHTTPWhen
invalid_request400The 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_cursor400The pagination cursor is not one we issued.
invalid_credentials401Missing, malformed, unknown or wrong-secret key.
token_expired401The key expired, or a rotation grace period ended.
insufficient_scope403The key was never granted this scope.
permission_revoked403The scope is on the key, but its owner lost the underlying account permission.
api_key_server_restricted403The 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_deactivated403The plan expired. Renew to use the server again - your files stay reachable in the file manager and over FTP.
plan_over_slot_limit403The 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_off403The server's owner has switched personal API tokens (or Discord /my) off. A personal token stops working when you get this.
section_not_shared403The server's owner does not share that part of your stats.
metrics_disabled404Metrics is switched off on GameGrid for now.
metrics_public_surfaces_off404Public Metrics pages (such as public leaderboards) are switched off on GameGrid for now.
legal_reacceptance_required403The 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_found404The server does not exist, belongs to someone else, or is outside this key's server restrictions. One response for all three.
resource_not_found404A non-server resource was not found.
route_not_found404No such route - or the capability is unavailable on your account. If a documented route returns this, contact support.
state_conflict409The 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_migration409The server is moving to a new host and cannot be controlled until it lands.
mod_job_cancel_too_late409You 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_finished409You asked to cancel a mod job that has already finished (succeeded, failed or been cancelled), so there is nothing to cancel.
job_not_cancellable409This kind of job cannot be cancelled once it has been queued - at present none can, so every POST /jobs/{jobId}/cancel answers this.
idempotency_conflict409The same Idempotency-Key was already used with a different body or query string.
idempotency_key_in_progress409A request with that key is still running.
webhook_limit_reached409Your account already has 100 webhooks, the most allowed. Delete one you no longer use, then register the new one.
payload_too_large413Over the 1 MB body limit.
unsupported_media_type415Wrong Content-Type for this endpoint.
validation_failed422Syntactically valid but semantically wrong - a port out of range, an unknown webhook event, a rejected webhook URL.
scope_exceeds_permissions422You asked for scopes you do not hold (key creation).
confirmation_required422A destructive action without a matching confirm value.
rate_limited429Too 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_reached429Your account already has 50 log streams open at once. Close one, then mint a new ticket.
internal_error500Something broke on our side. Quote the request_id.
capability_unsupported501The route exists, but this game or server cannot do it.
upstream_unavailable502The machine your server runs on could not be reached.
service_unavailable503Temporarily unavailable.
encryption_key_unavailable503The 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_unavailable503Your 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_timeout504The 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.

CodeHTTPWhen
vault_part_unknown400A load named a part the save does not hold or the platform cannot apply.
api_key_server_restricted403The 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_permission403The 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_disabled404The Vault is switched off on this platform: every Vault route answers this.
vault_full409The save would not fit in your Vault, or your Vault has no free space at all. details carries neededBytes, freeBytes and quotaBytes.
vault_too_large409The server's files or the backup are over 2 GB, the most one save can hold.
vault_game_mismatch409A 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_started409Save to Vault was asked for a server that has never run: it has no world yet, so nothing is started.
vault_no_world409A 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_running409A 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_mismatch409A 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_loading409You tried to delete a save while it is being loaded onto a server.
vault_item_not_ready409A download or a load named a save that is still being made, or failed.
vault_backup_not_verified409Keep in Vault named a backup that is not finished and verified.
vault_uploaded_backup409Keep in Vault named a backup you uploaded: the Vault only holds worlds saved from GameGrid servers.
vault_server_busy409A 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_running409A load (without whenReady) was asked for a server that is running, starting, restarting or stopping. Stop it first.
vault_server_migrating409A save or a load was asked while the server is moving to another machine.
vault_backup_already_running409Save to Vault was asked while a backup of the server is running.
vault_server_backup_quota_full409Save 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_busy409Two 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_saving409Keep 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.

TierRequests/minCovers
read300Every ordinary read, including the SSE streams themselves
poll600realtime-stats, config-event status and world map tiles (map/world/tile) - meant to be polled, or fetched many at a time
search60Server search by name; mod marketplace search
write120Ordinary writes
control30Lifecycle, maintenance, restore, rollback, config-event start/end
download20File and log downloads, metrics export, Metrics CSV datasets
upload10File uploads
stream60Minting a stream ticket
self60A 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:

HeaderMeaning
X-RateLimit-LimitThe limit for this tier
X-RateLimit-RemainingWhat is left, already counting this request
X-RateLimit-ResetSeconds remaining, not a Unix timestamp
X-RateLimit-TierWhich of the nine tiers applied
X-RateLimit-ScopeWhich limit was checked: key, key+server, org, or none
Retry-AfterOn a 429 only: seconds to wait

Rejected requests still count against your budget, so backing off properly matters. Read Retry-After rather 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.

SettingValue
Default page size50
Maximum page size100
Over the maximumClamped to 100 with a normal 200 - not an error
A malformed, zero or negative limit422 validation_failed
A cursor we did not issue400 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 pagination with has_more: false and no cursor. Two endpoints return no cursor at all: GET /servers/{id}/logs returns pagination: null, and GET /servers/{id}/console/history omits 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}/players cannot 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) and stale_reason (server_not_running, game_not_answering or host_unreachable). A live answer has none of them. With nothing that recent, the answer is 502 upstream_unavailable as 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.

SituationResult
Same key, same body and query stringThe original response is replayed, with Idempotency-Replayed: true
Same key, different body or query string409 idempotency_conflict
Same key, first request still running409 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 eventPayload
connected{ server_id, status } - sent immediately
historyThe recent buffer, as an array of lines
consoleOne line as it arrives
heartbeat{ ts } every 30 seconds
error{ error } - the stream then ends
Log stream eventPayload
------
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 with Last-Event-ID. A log stream ends after 60 minutes with error { error: "stream_lifetime_reached" }. A log client that falls behind gets gap; one that stays about 2 MB behind for 30 seconds gets error { 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 with stream_limit_reached (429). Handle error and 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:

EndpointReturns
GET /meta/scopesEvery scope and what each grants
GET /meta/error-codesThe error catalogue
GET /meta/rate-limitsThe nine tiers, the per-server control limit and the organization multiplier
GET /meta/gamesSupported games
GET /meta/webhook-eventsEvery event a webhook can subscribe to, plus the test ping
GET /openapi.jsonThe full machine-readable specification

Recent changes

  • 2026-10-26: responses match the published document. Five of these change what you receive:
  • GET /servers/{id}/files entries are { name, type, size, modified_at }; truncated: true (and pagination.has_more) means the folder has more than the 2000 entries listed.
  • GET /servers/{id}/player-lists entries carry name (the same value as label, which stays).
  • The billing-summary counts and backup size_bytes are numbers, not strings.
  • Battle Royale round_day is a date (2026-09-24), not a timestamp.
  • The DayZ held prizes total is 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 read data. Links, transactions, game events and settings history page with pagination.next_cursor (pass it back as ?cursor=), and links can be filtered with ?discord_user_id=. The shop and the catalogue now say has_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-access and 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_name and 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 can PUT a leaderboard back exactly as a GET gave it (the fields only an answer has are ignored); a boardId in the body that is not the one in the path is 400 invalid_request. Sending the same field in both spellings with different values is now 400 invalid_request (details.reason is conflicting_spellings) and changes nothing. Before, PUT .../economy/config and PUT .../economy/chat/bridge quietly kept one of the two, and PUT .../economy/shop/{itemId} failed with a 500. 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). POST and PUT .../dayz/game-events take duration_minutes, selects.hot_zones, rewards.kill_bonus.per_player_cap and so on, with the camelCase names still working. You can PUT back an event exactly as a GET gave it. A temporary area takes visible_to (admin, members or public). The timed steps are edited in the panel: leave selects.timeline out and they are kept; a PUT no longer deletes them.
  • Idempotency counts the query string. Reusing an Idempotency-Key on the same route with a different query (?path=a.txt, then ?path=b.txt) is now 409 idempotency_conflict instead 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-tickets or POST .../console/stream-tickets with 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, 404 or 422, instead of a bare 404 or a 502.
  • 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) and job_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, .../copy and .../move now answer 409 state_conflict with details.reason destination_exists and change nothing when the destination is already there. Send "overwrite": true in the body (it is optional, false by 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. total is the whole board; pageable_total is how many rows the cursor can reach and truncated is true when that is fewer. A cursor that is not one we handed out is 400 invalid_cursor instead 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.

GroupOpsWhat it covers
account10Account, organization, entitlements, roles, keys, audit trail
billing2Billing overview and plan list
bundles3Server bundles and slot usage
servers7List (filter by ?name= and ?state=), read, rename, status, connection address, real-time stats, migration status
lifecycle4Start, stop, restart, kill
lifecycle-history3State history and auto-restart
metrics58Live 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
jobs3List 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
console5History, 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)
logs5List, 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]
files15The 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)
settings12Schema, 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
players5Player counts and player lists
discovery2Server capabilities and the player-list catalogue
map10Live 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)
backups8List, read one, quota, restore-job status, create, label, delete, restore (no upload or download: those are on the panel's Backups page)
backup-schedule2Read and set the backup schedule
vault9Your 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
mods13Installed 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
maintenance9Verify, reinstall, wipe, delete savegames, update, history
config-events21Config 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-schedules10The 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)
economy34Wallets, shop, the item catalogue, purchases, transactions, links, chat bridge, kits
windrose-plus7Windrose Plus status, config, enable and disable, the RCON password, and turning UE4SS back on after GameGrid paused it (loader/unpark)
dayz22DayZ 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
webhooks8Register, 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-service8A player's own stats with a personal player token instead of an API key (/me/*: profile, sessions, travel, places, economy, events, deaths, one leaderboard)
tickets4Read and open support tickets
integrations2Link and unlink a Discord guild
meta5Unauthenticated discovery
openapi1The 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.