GameGrid
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.

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.

Capabilities - what each server can do

GET /servers/{id}/capabilities reports what this server actually supports, in ten groups: lifecycle, console, logs, files, settings, players, backups, mods, maintenance and economy.

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. This is the case on Windrose. If you are building a shop bot, read this flag and tell the buyer what to expect.

Two of these follow a switch the customer can flip: on a game whose economy is carried by an add-on, they are all false until the add-on is installed. Windrose is the case today — its economy is provided by Windrose Plus. Read capabilities per request rather than caching them for the life of a session.

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.
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.
server_deactivated403The plan expired. Renew to use the server again - your files stay reachable in the file manager and over FTP.
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.
server_in_migration409The server is moving to a new host and cannot be controlled until it lands.
idempotency_conflict409The same Idempotency-Key was already used with a different body.
idempotency_key_in_progress409A request with that key is still running.
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.
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.
upstream_timeout504The machine your server runs on did not answer in time.

GET /meta/error-codes returns this catalogue as JSON and needs no key. Note that server_deactivated is thrown by the platform but is not yet in that catalogue - it is listed above so your client can handle it.

Rate limits

Every route sits in one of eight tiers. All windows are 60 seconds.

TierRequests/minCovers
read300Every ordinary read, including the SSE streams themselves
poll600realtime-stats and config-event status - meant to be polled
search60Server search by name; mod marketplace search
write120Ordinary writes
control30Lifecycle, maintenance, restore, rollback, config-event start/end
download20File and log downloads, metrics export
upload10File uploads
stream10Minting a stream ticket

Two extra caps apply. 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 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 eight 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.

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.

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 - it is never required. 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.

SituationResult
Same key, same bodyThe original response is replayed, with Idempotency-Replayed: true
Same key, different body409 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.

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.

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.

There is no fixed stream lifetime and no concurrency cap. A stream stays open until you disconnect or the source ends. 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

All field names are snake_case. 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.

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 eight tiers, the per-server control limit and the organization multiplier
GET /meta/gamesSupported games
GET /openapi.jsonThe full machine-readable specification

Endpoint groups at a glance

The API is organised into 27 groups. 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
servers6List, read, rename, status, real-time stats, migration status
lifecycle4Start, stop, restart, kill
lifecycle-history3State history and auto-restart
metrics6Metrics, export, resource limits, diagnostics
jobs3Long-running job status and cancellation
console5History, command, clear, stream ticket, stream
logs5List, tail, download, stream ticket, stream
files15The full file manager, plus FTP and SFTP details
settings10Schema, values, validate, history, rollback, drift, worlds
players5Player counts and player lists
discovery2Server capabilities and the player-list catalogue
map7Live map: capabilities, players, entities, fog, leaderboard, share link, land claims
backups8List, create, upload, download, label, delete, restore
backup-schedule2Read and set the backup schedule
mods10Installed mods, marketplace search, install, toggle, reorder
maintenance9Verify, reinstall, wipe, delete savegames, update, history
config-events18Config presets and scheduled events - a 2x XP weekend, say
command-schedules10The command catalog and scheduled console commands: create, update, toggle, run now, history, runs
economy28Wallets, shop, purchases, transactions, links, chat bridge
windrose-plus6Windrose Plus status, config, enable and disable
webhooks8Register, update, test, redeliver, delivery history
tickets4Read and open support tickets
meta4Unauthenticated 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.