API reference

Every route of the platform API, version 1.

https://api.greenmindmedia.com/api/platform/v1

The same API is described for programs as an OpenAPI 3.1 document, at /openapi.json: every route, its credential, its parameters and body as JSON Schema, and the refusals it can answer. Hand it to an HTTP client or a code generator. Versioning says what may change within v1, and the Changelog what has.

Conventions

These hold for every route. Where a route differs, its own section says so.

Credentials

A route takes a server key (gmsk_...), a player's access token, either, or none, sent as Authorization: Bearer <credential>. See Authentication. A route that needs one kind refuses the other with forbidden.

A project's custom dashboard calls with a dashboard token, which names a person rather than a player. It reaches only the routes that let a dashboard in, each needing a permission in the environment that is checked on every call: the custom dashboards routes below, which take nothing else (dashboard in the summary), and GET /config/values, GET /config/tables, GET /config/tables/:key and GET /flags, for whoever may open a dashboard there. Every other route refuses it with forbidden.

Every route acts in the environment its credential belongs to. Only the routes that take no credential name an environment themselves: the EOS sign-in, in its body, and the crash reporter's upload, in its path.

Paths

  • A player's own things are under /players/me. With a player's token, :playerId is always me, and reaches only that player; naming any id, even their own, is forbidden. With a server key, :playerId is a player's id, and a player of another environment is not-found.
  • What belongs to the environment is at the top level: /config, /flags, /legal/documents, /blog, /realms, /definitions, /stats, /sessions, /support, /commerce.
  • The thing acted on is in the path; the body says what to set, or how. A session's seat is /sessions/:sessionId/players/:playerId, an item is /players/:playerId/items/:itemId.
  • An action that is not a read, a write or a delete is a POST with the verb last: /sessions/:sessionId/leave, /players/:playerId/items/:itemId/open, /commerce/grants/:grantId/ack.
  • Sign-ins are under /auth.
  • The one route that names its environment in its path is the crash reporter's, POST /crash-reports/:appKey, since the crash reporter cannot send a credential.
  • Trailing slashes make no difference.

Requests

  • Bodies are JSON, sent with Content-Type: application/json, and strict: a field the route does not take is refused, as is a body that is not JSON. A body is at most 64 KB unless its route says otherwise (16 KB for the sign-ins, the licence routes and the game services, 512 KB for a save, 20 MB for a crash upload); a bigger one is refused with 413 payload-too-large before it is read.
  • Query parameters are strict too: one the route does not take is refused.
  • Strings are trimmed where a route says so, and limits count characters.
  • Ids of players, sessions, items and the like are 1 to 64 characters.
  • Dates are ISO 8601 strings in UTC.
  • Services. A route listed with a service answers service-disabled when the project has not switched it on.
  • Review. Every route that acts for an environment, whatever credential it takes, and the sign-ins and crash uploads that name their environment, answer 403 project-not-approved while the project waits for GreenMind's review, after GreenMind did not approve it, and while GreenMind has it suspended. The public key sets, the OpenAPI document and the licence routes stay open.

Answers

Request Answer
Reads one thing 200 with the thing itself, not wrapped
Reads a list 200 with a page: { "items": [...], "next": "..." or null }
Makes something 201 with what it made; a route that may find instead of make answers 200 when it found
Changes something 200 with it as it now is
Does something, or deletes, and is done 204, with no body. Deleting what is not there is not an error, where the route says so

A list may carry, beside items and next, figures about the whole list: a player's save list says totalBytes, and a list of what one inventory holds says which inventory that was.

Pagination

Every list pages the same way:

Query Rules
limit Optional, 1 to 200; 50 when absent. How many items to answer at most
cursor Optional. The next of the page before; absent for the first page

next is null on the last page. A cursor is opaque: store it and send it back, never build or read one. What it holds differs from list to list and may change; one a list did not make is refused with invalid-request, naming query.cursor. A page can hold fewer than limit items without being the last, so go on until next is null.

The one exception is a change feed, GET /commerce/grants/changes, whose next is never null: it is where to ask from next, and an empty page means you are up to date.

Refusals

Every refusal has one shape:

{
  "error": "invalid-request",
  "message": "body.displayName: Too small: expected string to have >=1 characters",
  "details": { "field": "body.displayName" }
}

error is a code for your program to branch on, from the table under Errors, and never changes meaning. message is a sentence for a person and may be reworded at any time. details, where there is anything to say, carries the figures behind the refusal:

Field With
field A validation failure: body.<field>, query.<name>, path.<name> or header.<name>
quota, limit quota-exceeded: what the limit counts, and how many are allowed
retryAfterSeconds rate-limited: the same as the Retry-After header
held, requested, allowance, currencyKey, missing, problems The game services' refusals

A path the API does not have answers 404 not-found. A 5xx answer other than 502 and 503 is a fault on our side: retry with backoff.

Rate limits

Each environment's requests are counted against your org's plan, which the console's Usage page shows:

  • Per environment, per minute: every request made with any of the environment's credentials, the EOS sign-in included once its token has been checked.
  • Per credential, per minute: one player's token (and their refreshes), or one server key, so one client cannot use its environment's whole minute.
  • Per environment, per day (UTC).

A request with no credential, or one that is not good (a sign-in, a public key set, a licence call, a crash upload, a mistyped key), is held to limits of its own for the address it came from: 600 a minute across every environment, and for an EOS sign-in 120 a minute in the environment it names. Those count nothing of the environment's, so nobody who knows your app key can use up your minute or your day.

Every answer says where its caller stands against the limit nearest to refusing them:

Header Meaning
X-RateLimit-Limit The most that limit allows in its window
X-RateLimit-Remaining What is left of it after this request
X-RateLimit-Reset Seconds until its window starts again

Minutes are the clock's minutes, not rolling windows. A request over a limit is refused with 429 rate-limited and a Retry-After header, the seconds until the window is over. A refused request is not counted, but retrying at once gets nothing: wait for Retry-After, then back off further if it is refused again. Some routes have limits of their own (support filings, Steam identification, crash uploads); they answer the same way.

Making things past what the plan allows is refused with quota-exceeded, never rate-limited.

Idempotency

A route marked Idempotency-Key takes a header naming the one request, 1 to 128 printable ASCII characters, the same on every retry of it. A retry answers what the first answered and does nothing again; the same key for a different request is refused with idempotency-conflict. Where the key is required, a request without one is refused with invalid-request, naming header.Idempotency-Key. Derive the key from what the request is for (match-8812:reward), never from a clock.

Conditional reads

Config values, config tables and flags answer with an ETag. Send it back as If-None-Match and the answer is 304 Not Modified, with no body, until something changes.

Response shapes

Tokens

interface PlatformTokens {
  accessToken: string
  refreshToken: string
  expiresIn: number // seconds until the access token expires: 3600
  player: PlatformPlayer
}

Player

interface PlatformPlayer {
  id: string
  displayName: string | null
  createdAt: string
}

Session

interface PlatformSession {
  id: string
  status:
    'CONFIGURING' | 'PREPARING_INSTANCE' | 'AWAITING_PLAYERS' | 'PLAYING' | 'COMPLETE' | 'ERROR'
  gameMode: string
  map: string
  conclusion: string | null
  /** How many players it seats: the project's seats, held to the org's plan */
  seats: number
  players: { playerId: string; playerName: string; isHost: boolean; isReady: boolean }[]
  createdAt: string
}

Realtime

interface PlatformRealtimeInfo {
  /** The sessions socket's address, on the API's own host */
  url: string
  /** The protocol version, `v` in every frame */
  protocol: number
  limits: {
    maxMessageBytes: number
    messagesPerSecond: number
    messagesPerMinute: number
    maxSubscriptions: number
  }
}

Config row

interface ConfigRow {
  key: string
  groupName: string | null
  type: string // 'string' | 'int' | 'float' | 'bool' for values made in the console
  valueStr: string | null
  valueInt: number | null
  valueFloat: number | null
  valueBool: boolean | null
  GmLevelRateConfig: object | null // always null for values made in the console
}

Config table

interface ConfigTable {
  key: string
  name: string
  description: string | null
  groupName: string | null
  columns: { key: string; label: string; type: 'string' | 'int' | 'float' | 'bool' }[]
  // In the editor's order. An empty cell is null or absent.
  rows: {
    key: string
    sortOrder: number
    cells: Record<string, string | number | boolean | null>
  }[]
}

Legal document and a player's legal standing

interface LegalDocument {
  document: string // your key for it: 'terms', 'privacy'
  title: string
  url: string
  version: string // the current version, the one a player accepts
  required: boolean
}

interface LegalStanding {
  documents: (LegalDocument & {
    acceptedVersion: string | null // the latest version the player accepted
    acceptedAt: string | null
    current: boolean // whether they accepted the current version
  })[]
  outstanding: string[] // keys of required documents not accepted at their current version
}

Tour

interface Tour {
  id: string
  name: string
  completed: boolean
  completedAt: string | null
}

Save

interface SaveEntry {
  key: string
  version: number // 1 when first written, one more with every write
  sizeBytes: number
  updatedAt: string
}

interface Save extends SaveEntry {
  data: JsonValue // whatever was written: any JSON value but null
  createdAt: string
}

interface SaveList {
  items: SaveEntry[] // ordered by key
  next: string | null
  totalBytes: number // every slot together, not only this page's
  limitBytes: number // 5,242,880
}

Summary

Method Path Credential Service
GET /openapi.json none
POST /auth/eos none players
POST /auth/refresh none
POST /auth/server server players
GET /.well-known/jwks.json none
GET /players/:playerId either
PATCH /players/:playerId either
DELETE /players/:playerId server
POST /players/:playerId/sign-out either
GET /config/values either config
GET /config/tables either config
GET /config/tables/:key either config
GET /flags either flags
GET /realtime player sessions
POST /sessions player sessions
GET /sessions/:sessionId either sessions
POST /sessions/:sessionId/join player sessions
POST /sessions/:sessionId/leave player sessions
POST /sessions/:sessionId/ready player sessions
POST /sessions/:sessionId/players/:playerId/kick player sessions
POST /sessions/:sessionId/players/:playerId/promote player sessions
POST /sessions/:sessionId/status server sessions
GET /legal/documents either legal
GET /players/:playerId/legal either legal
GET /players/:playerId/legal/outstanding either legal
POST /players/:playerId/legal/accept player legal
GET /players/:playerId/tours player tours
POST /players/:playerId/tours/:tourId/complete player tours
GET /blog/posts either blog
GET /blog/posts/:slug either blog
GET /blog/categories either blog
GET /blog/tags either blog
POST /support/requests player support
POST /support/reports player support
GET /players/:playerId/data either persistence
GET /players/:playerId/data/:key either persistence
PUT /players/:playerId/data/:key either persistence
DELETE /players/:playerId/data/:key either persistence
POST /crash-reports/:appKey none crashes
POST /crash-symbols/uploads server crashes
GET /crash-symbols/files server crashes
GET /commerce/catalog player commerce
POST /commerce/orders player commerce
POST /commerce/steam/identify player commerce
POST /commerce/steam/authorizations player commerce
POST /commerce/orders/reconcile player commerce
GET /players/:playerId/entitlements either commerce
GET /commerce/grants server commerce
GET /commerce/grants/changes server commerce
POST /commerce/grants/:grantId/ack server commerce
POST /auth/dashboard none
GET /dashboard/session dashboard
GET /players dashboard players
GET /licence/jwks.json none
POST /licence/activate licence key
POST /licence/check licence key
POST /licence/release licence key

The Players service is always on, so the routes without a listed service need nothing switched on. The licence routes belong to no environment: they are for the GmCommon plugin, with the org's licence key as the credential, and are described in GmCommon. The commerce routes' flow is described in Commerce.

The game services' routes, described under Game services below:

Method Path Credential Service
GET /realms either realms
GET /realms/current either realms
GET /definitions/realms server realms
GET /definitions/currencies server inventory
GET /definitions/item-types server inventory
GET /definitions/card-types server cards
GET /definitions/card-templates server cards
GET /definitions/skill-trees server progression
GET /players/:playerId/inventories either inventory
GET /players/:playerId/inventories/:inventory either inventory
GET /players/:playerId/inventories/:inventory/balances either inventory
GET /players/:playerId/inventories/:inventory/items either inventory
GET /players/:playerId/inventories/:inventory/item-stacks either inventory
GET /players/:playerId/ledger either inventory
POST /players/:playerId/purchases/items either inventory
POST /players/:playerId/items/:itemId/use either inventory
POST /players/:playerId/item-stacks/use either inventory
POST /players/:playerId/items/:itemId/open either inventory
POST /players/:playerId/currencies/:currencyKey/credit server inventory
POST /players/:playerId/currencies/:currencyKey/debit server inventory
POST /players/:playerId/currencies/:currencyKey/refund server inventory
POST /players/:playerId/currencies/:currencyKey/consume server inventory
POST /players/:playerId/items/grant server inventory
POST /players/:playerId/items/:itemId/consume server inventory
POST /players/:playerId/item-stacks/consume server inventory
GET /players/:playerId/inventories/:inventory/cards either cards
GET /players/:playerId/inventories/:inventory/card-stacks either cards
POST /players/:playerId/purchases/cards either cards
GET /players/:playerId/decks either cards
POST /players/:playerId/decks either cards
GET /players/:playerId/decks/:deckId either cards
PATCH /players/:playerId/decks/:deckId either cards
DELETE /players/:playerId/decks/:deckId either cards
PUT /players/:playerId/decks/:deckId/cards/:cardId either cards
DELETE /players/:playerId/decks/:deckId/cards/:cardId either cards
PUT /players/:playerId/decks/:deckId/types/:cardTypeKey either cards
GET /players/:playerId/decks/:deckId/validation either cards
POST /players/:playerId/cards/grant server cards
POST /players/:playerId/cards/:cardId/consume server cards
POST /players/:playerId/card-stacks/grant server cards
POST /players/:playerId/card-stacks/consume server cards
GET /players/:playerId/xp either progression
POST /players/:playerId/xp server progression
GET /players/:playerId/skill-trees either progression
GET /players/:playerId/skill-trees/:treeKey either progression
POST /players/:playerId/skill-nodes/:nodeKey/unlock either progression
GET /players/:playerId/unlocks either progression
PUT /players/:playerId/unlocks/:key server progression
DELETE /players/:playerId/unlocks/:key server progression
GET /players/:playerId/stats either stats
GET /stats either stats
POST /players/:playerId/stats/:key/increment server stats
POST /stats/:key/increment server stats

Sign-in

POST /auth/eos

Signs a player in with an Epic Online Services id token, in an environment that accepts EOS. No credential.

Field Type Rules
appKey string Required, 1 to 64 characters. The environment's app key
idToken string Required, 1 to 8,000 characters. The EOS Connect id token of the player's product user

Answers Tokens: 201 when the sign-in made the player (the first time their product user id is seen), 200 after that.

Refusals: provider-disabled when EOS is not switched on for the environment; unauthorized when the token cannot be verified or is for another deployment; service-disabled when no environment has that app key; quota-exceeded for a new player past the plan.

POST /auth/refresh

A new pair of tokens for a refresh token. No credential.

Field Type Rules
refreshToken string Required, 1 to 4,000 characters

Answers 200 with Tokens. Refusals: unauthorized when the refresh token is invalid or expired, its player no longer exists, or the player has been signed out everywhere since it was issued.

POST /auth/server

Your server vouches for a player by your own id for them. Server key.

Field Type Rules
subject string Required, 1 to 200 characters. Your id for the player
displayName string Optional, 1 to 40 characters after trimming. Replaces the player's name when given

Answers Tokens: 201 when the sign-in made the player, 200 when it found them. Refusals: quota-exceeded for a new player past the plan.

GET /.well-known/jwks.json

The public keys player tokens are signed with, as a JSON Web Key Set. No credential. Your server uses it to check a player's token itself; see Authentication.

{
  "keys": [
    {
      "kty": "OKP",
      "crv": "Ed25519",
      "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
      "kid": "kPrK_qmxVWaYVA9wwBF6Iuo3vVzz7TxHCTwXBygrS4k",
      "alg": "EdDSA",
      "use": "sig"
    }
  ]
}

While a key is being replaced the set holds two: the new one first, and the old one until the last token it signed has expired. Cache the set, and fetch it again when a token names a kid you do not have.

Players

GET /players/:playerId

One player. Answers 200 with Player, or not-found. With a player's token, GET /players/me is also the way for your server to check a token a client gives it: the token is good if this answers.

PATCH /players/:playerId

Changes a player's display name.

Field Type Rules
displayName string Required, 1 to 40 characters after trimming

The name passes a word filter for names, which stars out words it does not allow. Answers 200 with Player, showing the name as stored.

DELETE /players/:playerId

Deletes a player at their request. Server key. Their display name, identities, saves, tours, seats, game state and what they hold from purchases go, and every token they hold stops working; the same id signing in again is a new player. Answers 204, or not-found.

POST /players/:playerId/sign-out

Signs a player out everywhere: every token they hold, access and refresh alike, stops working within moments. me with a player's token, a player's id with a server key. Nothing they own or saved is touched. Answers 204, or not-found.

Config and flags

Each of these answers with an ETag, and 304 to an If-None-Match naming it, until something changes. See Config and flags.

GET /config/values

The environment's config values, ordered by key, a page at a time. Either credential. Service: config.

Query Rules
groupName Optional, 1 to 120 characters. Only values in that group

Answers 200 with a list of ConfigRow.

GET /config/tables

The environment's config tables, ordered by key, each with its columns and rows, a page at a time. Either credential. Service: config. Answers 200 with a list of ConfigTable.

GET /config/tables/:key

One config table by its key. Either credential. Service: config. Answers 200 with ConfigTable, or not-found.

GET /flags

The environment's feature flags, ordered by name, a page at a time. Either credential. Service: flags. Answers 200 with a list of { "name": string, "isEnabled": boolean }.

Sessions

Every sessions route needs the sessions service. See Sessions for the lifecycle, and Realtime for the socket that tells every seat what changed.

GET /realtime

Where the player's client opens the sessions socket, and the limits it is held to there. Player token. Answers 200 with Realtime. Read it when your game starts rather than building the address in: it may move, and the limits follow your org's plan.

POST /sessions

Opens a lobby with the caller seated as host. Player token.

Field Type Rules
gameMode string Required, 1 to 60 characters after trimming
map string Required, 1 to 60 characters after trimming
playerName string Optional, 1 to 40 characters after trimming. The seat's name; defaults to the player's display name, or Player
uniqueNetId string Optional, up to 200 characters. Stored with the seat, not returned

Answers 201 with Session, status CONFIGURING.

GET /sessions/:sessionId

Reads a session. Either credential. Answers 200 with Session, or not-found.

POST /sessions/:sessionId/join

Seats the caller. Player token.

Field Type Rules
playerName string Optional, as for POST /sessions
uniqueNetId string Optional, as for POST /sessions

Send {} when giving neither. Answers 200 with Session. Refusals: not-found for no such session; conflict when the session is full (its seats, with details.limit), the caller is already seated, or the session is past CONFIGURING.

POST /sessions/:sessionId/leave

Gives up the caller's seat, in any status. Player token. Any body is ignored. Answers 204. Refusals: not-found when the session does not exist or the caller is not seated. The host role is not passed on.

POST /sessions/:sessionId/ready

Player token.

Field Type Rules
isReady boolean Required

Answers 200 with Session. When the caller readies and every other seated player is ready, the status in the answer is PREPARING_INSTANCE. Refusals: not-found when the session does not exist or the caller is not seated; conflict when the session is past CONFIGURING.

POST /sessions/:sessionId/players/:playerId/kick

The host removes a seated player. Player token. Answers 200 with Session. Refusals: not-found when the session does not exist or the caller is not seated; forbidden when the caller is not the host; conflict when the player is not seated or the session is past CONFIGURING.

POST /sessions/:sessionId/players/:playerId/promote

The host makes another seated player host. Player token. Answers 200 with Session. Refusals as for kick.

POST /sessions/:sessionId/status

Your server reports the match's progress. Server key.

Field Type Rules
status string Required: AWAITING_PLAYERS, PLAYING, COMPLETE or ERROR
conclusion string Optional, 1 to 60 characters after trimming. Kept with COMPLETE
error string Optional, up to 500 characters. Kept with ERROR

Allowed moves: PREPARING_INSTANCE to AWAITING_PLAYERS, PLAYING or ERROR; AWAITING_PLAYERS to PLAYING or ERROR; PLAYING to COMPLETE or ERROR. Answers 200 with Session. Refusals: not-found; conflict for any other move, or when another report changed the session first.

Every legal route needs the legal service. See Other services for how a game uses them.

Every legal document of the environment, ordered by key. Either credential. Answers 200 with a list of LegalDocument.

Where one player stands. Answers 200 with LegalStanding, or not-found.

The required documents whose current version the player has not accepted. Answers 200 with a list of LegalDocument; an empty list means the player may play.

POST /players/:playerId/legal/accept

Records the signed-in player's acceptance of the versions they were shown. Player token, so :playerId is me.

Field Type Rules
documents array Required, 1 to 20 of { "document": string, "version": string }, each the key and current version of one document

Answers 200 with LegalStanding. Accepting a version already accepted changes nothing. Refusals: invalid-request when a document does not exist, or a version is not its current one.

Tours

Every tour route needs the tours service, and a player's token: :playerId is me.

GET /players/:playerId/tours

The environment's switched-on tours, ordered by name, with whether the player has finished each. Answers 200 with a list of Tour.

POST /players/:playerId/tours/:tourId/complete

Marks a tour finished for the player. Any body is ignored. Answers 204, again if it was finished already. Refusals: not-found for a tour that is not the environment's.

Blog

Every blog route needs the blog service, and reads published posts only: a draft, or a post scheduled for later, is not-found until it is published. Post shapes are those the console's blog pages write: a summary carries slug, title, subtitle, excerpt, publishedAt, readingMinutes, featured, authorName, category, tags and cover; a full post adds content (the post's document, as JSON), seoTitle, seoDescription and updatedAt.

GET /blog/posts

Published posts, newest first, a page at a time. Either credential. A page reads on from the last post of the page before, so a post published while you read moves nothing you have yet to reach.

Query Rules
category Optional. A category's slug: only its posts
tag Optional. A tag's slug: only posts with it

Answers 200 with a list of post summaries. Refusals: not-found for a category or tag that does not exist.

GET /blog/posts/:slug

One published post. Either credential. Answers 200 with { "post": Post, "related": PostSummary[] }, where related is more to read, from the same category first. Refusals: not-found.

GET /blog/categories

The categories with at least one published post, in the order set in the console. Either credential. Answers 200 with a list of { "slug", "name", "description" }.

GET /blog/tags

The tags on at least one published post, ordered by name. Either credential. Answers 200 with a list of { "slug", "name" }.

Support

Every support route needs the support service, and a player's token. A player can file ten requests and reports an hour between them; the next is refused with 429 rate-limited, and Retry-After says how many seconds until the hour is over.

POST /support/requests

Files a support request for the signed-in player in the environment's inbox.

Field Type Rules
subject string Required, 3 to 120 characters after trimming
message string Required, 10 to 5,000 characters after trimming
contactEmail string Optional, an email address, up to 254 characters. Where staff can answer

Answers 201 with { "id" }, the request in the inbox.

POST /support/reports

The signed-in player reports another player of the environment.

Field Type Rules
playerId string Required, 1 to 64 characters. The player reported
reason string Required, 2 to 60 characters after trimming. Your own word: cheating, name
details string Optional, up to 2,000 characters after trimming. What the reporter wrote

Answers 201 with { "id" }. Refusals: not-found for a player who is not the environment's; invalid-request for a player reporting themselves.

Player saves

Every save route needs the persistence service. A key is 1 to 120 letters, digits and . _ : -. A slot whose key starts server: is written only with a server key; a player's token reads it, and is refused with forbidden if it writes or deletes it. See Player saves.

GET /players/:playerId/data

The player's slots, ordered by key, without their values. Answers 200 with SaveList, or not-found for a player who is not the environment's.

GET /players/:playerId/data/:key

One slot. Answers 200 with Save, or not-found for a slot never written.

PUT /players/:playerId/data/:key

Writes one slot, making it if it is new.

Field Type Rules
data JSON Required. Any JSON value but null, up to 256 KB written compactly
expectedVersion integer Optional, 0 or more. Write only if the slot is at this version now; 0 for a slot that must not exist. Absent, always write

Answers SaveEntry, carrying the new version: 201 when the write made the slot, 200 when it replaced it. Refusals: conflict when the slot is not at expectedVersion; quota-exceeded when the player's saves would pass 5 MB (details.quota playerDataBytesPerPlayer) or the environment's would pass its plan (playerDataBytes); invalid-request for a value over 256 KB, null, or a malformed key; 413 payload-too-large for a body over 512 KB.

DELETE /players/:playerId/data/:key

Empties a slot. Answers 204, whether or not it held anything.

Crash reports

POST /crash-reports/:appKey

No credential; service crashes. Where Unreal's crash reporter uploads, named as its DataRouterUrl: the body is the compressed crash as the reporter sends it, and the query parameters are the ones the reporter adds (AppID, AppVersion, AppEnvironment, UploadType, UserID). Answers 200 with { "received": true } as soon as the upload is kept; it is read into the environment's crashes moments later. Refusals: 413 payload-too-large over 20 MB; invalid-request for an empty body; 429 rate-limited, with Retry-After, when one address has sent more than 10 to the environment in ten minutes, or everybody together more than 600 in an hour; service-disabled for an environment that has not switched crash reports on or does not exist; 503 unavailable where crash reports cannot be collected. See Crash reports.

POST /crash-symbols/uploads

Server key; service crashes. An upload URL for one of a build's debug files, so its crashes' pages can name the files that resolve their offsets. See Crash reports.

Field Type Rules
label string The build's Symbols, as its crash reports show it. Letters, digits and * + . _ -, up to 200
version string The build's GmProjectVersion. Letters, digits and + . _ -, up to 64
fileName string The file's own name, no folders. Letters, digits and + . _ -, up to 200
sizeBytes number The file's exact size, up to 4 GB
sha256 string The file's SHA-256, 64 lower-case hex digits

Answers 200 with { url, headers, key, expiresAt }: PUT the file to url with exactly headers before expiresAt (an hour). S3 refuses a body of another size or digest. No part may contain ... Refusals: 503 unavailable where symbols cannot be published.

GET /crash-symbols/files

Server key; service crashes. The symbol files the environment has published, by label, then version, then name, a page at a time, narrowed by the optional query parameters label and version. Answers 200 with a list of { label, version, name, sizeBytes, updatedAt }. Refusals: 503 unavailable where symbols cannot be published.

Custom dashboards

How a project's custom dashboard signs its people in, and what it reads that a game does not.

POST /auth/dashboard

No credential. Signs a person in to the dashboard of an environment's project with the code GreenMind sent back to the dashboard's /auth/callback.

Field Type Rules
appKey string Required, 1 to 64 characters. The environment to sign in to, one of the project's
code string Required, up to 2,048 characters. The code the callback was given
codeVerifier string Required, 43 to 128 characters of A-Z a-z 0-9 . _ ~ -. The PKCE verifier of the sign-in
nonce string Required, 16 to 256 characters. The nonce the sign-in was started with

Answers 200 with { accessToken, expiresIn, appKey, permissions }: the dashboard token, the seconds until it runs out (3600), the environment, and what the person may do there. Refusals: not-found where the environment's project has no dashboard; 401 unauthorized where the code is not good, has been used, or answers another sign-in; forbidden for somebody who may not open a dashboard in the environment, or whose GreenMind account is closed; 503 unavailable where this deployment hosts no custom dashboards.

GET /dashboard/session

Dashboard token. Who the token is for and what they may do in its environment, asked of the permission engine now. Answers 200 with { appKey, account: { id }, permissions }.

GET /players

Dashboard token, for somebody who may view players. The environment's players, newest first, a page at a time, narrowed by the optional query parameter q (part of a display name, or a player's id or an identity's subject exactly; never an email). Answers 200 with a list of { id, displayName, identities, hasGreenMindAccount, createdAt, lastSeenAt }.

Commerce

Every commerce route needs the commerce service. The flow, the grants a product can give and what each refusal means are in Commerce.

Route Credential Body or query Answers
GET /commerce/catalog player ?provider=steam { products }, priced for the player
POST /commerce/orders player { sku, provider, language }; Idempotency-Key, optional 201 with the order
POST /commerce/steam/identify player { ticket, identity: "gm-commerce" } { steamId }
POST /commerce/steam/authorizations player { steamOrderId, authorized } { order, entitlements }
POST /commerce/orders/reconcile player none { orders, entitlements }
GET /players/:playerId/entitlements either the page's A list of entitlements
GET /commerce/grants server the page's A list of grants not yet acknowledged
GET /commerce/grants/changes server the page's A list of changes; next is never null
POST /commerce/grants/:grantId/ack server none The grant, acknowledged

POST /commerce/steam/identify is limited to ten calls a player in ten minutes, each of which asks Steam; the next is refused with rate-limited.

Game services

Services realms, inventory, cards, progression and stats, as each route's table row says. See Game services for what each thing is. A player's token gets forbidden from every route marked server, whatever it sends.

Keys (of currencies, item types, card types, templates, trees, nodes, unlocks and stats, and reason) are 1 to 128 letters, digits and . _ : -, starting with a letter or digit. Ids are 1 to 64 characters. Amounts are whole numbers from 1 to 1,000,000,000; quantities from 1 to 1,000. A body is at most 16 KB.

Choosing an inventory. A path's :inventory is account, current (the current realm's) or an inventory's id; account and current make the inventory if the player has none yet. A body that puts something somewhere takes inventoryId (one of the player's) or realmId (a realm's id, or null for the account); with neither, the definition decides: the account for what is held on the account, the current realm for what is held per realm or has no place of its own (cards).

Shapes

interface Realm {
  id: string
  number: number // unique here; 0 is the permanent realm
  key: string
  name: string
  enabled: boolean
  visible: boolean
  startAt: string
  endAt: string | null
}

interface Inventory {
  id: string
  realmId: string | null // null for the account inventory
}

interface Movement {
  transactionId: string
  inventoryId: string
  currencyKey: string
  amount: number // the signed change to the stored figure
  balanceAfter: number // for a consumed currency, what has been spent
  replayed: boolean // the Idempotency-Key named a movement already made
}

interface LedgerEntry {
  id: string
  inventoryId: string
  currencyKey: string
  amount: number
  balanceAfter: number
  reason: string
  reference: string | null
  createdAt: string
}

interface Item {
  id: string
  inventoryId: string
  itemTypeKey: string
  grantKey: string | null
  consumedAt: string | null
  data: object // your own fields
}

interface Card {
  id: string
  inventoryId: string
  cardTypeKey: string
  cardTemplateKey: string | null
  grantKey: string | null
  consumedAt: string | null
  data: object
}

interface Deck {
  id: string
  inventoryId: string
  name: string
  entries: { id: string; cardId: string | null; cardTypeKey: string | null; count: number }[]
}

interface Purchase<T> {
  replayed: boolean // true when the Idempotency-Key named a purchase already made
  charges: Movement[]
  granted: T | null // null when replayed
}

interface SkillTree {
  key: string
  name: string
  description: string
  hidden: boolean
  nodes: {
    key: string
    treeKey: string
    name: string
    description: string
    icon: string | null
    priceRule: 'all' | 'any'
    parents: string[] // nodes that must all be unlocked first
    prices: { currencyKey: string; amount: number }[]
    unlocked: boolean
    unlockable: boolean // not unlocked, and every parent is
  }[]
}

interface Stat {
  key: string
  value: string // a 64-bit counter, as a decimal string
}

Realms and inventories

  • GET /realms answers a list of every open Realm, newest first; GET /realms/current the current one, or not-found while none is open.
  • GET /players/:playerId/inventories answers a list of every Inventory the player has. GET /players/:playerId/inventories/:inventory answers one.
  • GET /players/:playerId/inventories/:inventory/balances answers a list of { currencyKey, amount }, with inventory beside it.
  • GET /players/:playerId/inventories/:inventory/items answers a list of the Items held one by one and not yet used, and .../item-stacks a list of the counted ones, { itemTypeKey, quantity }; each with inventory beside it.

GET /players/:playerId/ledger

The player's currency movements, newest first, a page at a time: a list of LedgerEntry.

Query Rules
currencyKey Optional. Only this currency's
inventoryId Optional. Only this inventory's

POST /players/:playerId/currencies/:currencyKey/credit, /debit, /refund, /consume

Server key, with an Idempotency-Key (required). Moves one currency: a retry answers the first receipt with replayed: true.

Field Type Rules
amount integer Required. How much, always positive: the route is the direction
allowance integer consume only, and required there: the most the player may have spent in all, after it
reason string Required. A key naming the flow, such as match.settlement
reference string Optional, 1 to 200 characters. What it was about: a match, an order
inventoryId string Optional. See Choosing an inventory
realmId string Optional, or null for the account

credit and debit are for balance currencies, consume for consumed ones; refund lowers what a consumed currency has spent, or credits a balance. Answers 200 with Movement. Refusals: insufficient-balance, allowance-exceeded, balance-cap, idempotency-conflict, invalid-request for the wrong kind of currency, not-found for no such currency.

Items

Granting and taking away items, counted cards and XP needs an Idempotency-Key, the same on every retry of that one request: a retry answers the first answer again and gives or takes nothing more.

POST /players/:playerId/items/grant, server key, with an Idempotency-Key.

Field Type Rules
itemTypeKey string Required
quantity integer Optional, 1 to 1,000; 1 when absent
grantKey string Optional, one-by-one types only, with a quantity of 1: granting again under it answers the first
data object Optional, one-by-one types only, up to 1 KB written compactly
inventoryId string Optional
realmId string Optional, or null

Answers { "inventoryId", "itemTypeKey", "stack": { "itemTypeKey", "quantity" } | null, "items": Item[], "alreadyGranted": boolean }. Refusals: disabled, owned-limit, invalid-request (a grant key or data on a counted type, or data over 1 KB).

POST /players/:playerId/items/:itemId/consume, server key, with an Idempotency-Key: takes one item away. Answers { "item": Item }. POST /players/:playerId/item-stacks/consume takes some of a counted type away, { "itemTypeKey", "quantity"?, "inventoryId"?, "realmId"? }, and answers { "stack": { "itemTypeKey", "quantity" } }. Refusals: already-consumed, insufficient-balance.

POST /players/:playerId/items/:itemId/use, either: uses, and so consumes, one item. POST /players/:playerId/item-stacks/use, either, with { "itemTypeKey", "inventoryId"?, "realmId"? }: uses one of a counted type. Each answers { "itemTypeKey", "item": Item | null }. Refusals: not-usable, locked (with details.missing), disabled, already-consumed.

POST /players/:playerId/items/:itemId/open, either. Opens a container (an item whose type's category is container), once. Answers as use. Refusals: not-usable for anything else, already-consumed.

Buying

POST /players/:playerId/purchases/items, either. An optional Idempotency-Key makes a retry answer the first purchase.

Field Type Rules
itemTypeKey string Required
quantity integer Optional, 1 to 100: every price is multiplied by it
inventoryId string Optional. The inventory it goes to; its realm pays what is held per realm
realmId string Optional. The realm that pays what is held per realm
payWith string Optional. For a price list paid with any one price: the currency to pay in
allowances object Server key only: what the game allows of each consumed currency, by its key

Answers 200 with Purchase of the item grant. Refusals: not-purchasable, disabled, insufficient-balance, owned-limit, invalid-request for a consumed currency without its allowance, forbidden for allowances from a player's token.

POST /players/:playerId/purchases/cards, either. cardTypeKey (required), as (instance, the default, for a card of its own, or stack for one more of a counted type), inventoryId, realmId, payWith; with a server key also cardTemplateKey, data (up to 1 KB) and allowances. Answers Purchase of { "card": Card, "alreadyGranted" } or of a card stack { "inventoryId", "cardTypeKey", "quantity" }.

Cards and decks

GET /players/:playerId/inventories/:inventory/cards answers a list of the Cards held one by one, and .../card-stacks a list of the counted ones, { "inventoryId", "cardTypeKey", "quantity" }; each with inventory beside it.

POST /players/:playerId/cards/grant, server key. cardTypeKey (required), cardTemplateKey, grantKey, data (up to 1 KB), inventoryId, realmId. Answers { "card": Card, "alreadyGranted": boolean }.

POST /players/:playerId/cards/:cardId/consume, server key. Takes the card away, and out of every deck. Answers Card.

POST /players/:playerId/card-stacks/grant and /card-stacks/consume, server key, with an Idempotency-Key. { "cardTypeKey", "quantity", "inventoryId"?, "realmId"? }, quantity 1 to 1,000. Answers the stack.

Decks, either credential, a player's token for its own:

  • GET /players/:playerId/decks[?inventoryId=] answers a list of Deck.
  • POST /players/:playerId/decks with { "inventoryId", "name" } (name 1 to 60 characters after trimming) answers 201 with Deck; owned-limit past 50 decks.
  • GET and PATCH ({ "name" }) on /players/:playerId/decks/:deckId answer Deck; DELETE answers 204.
  • PUT /players/:playerId/decks/:deckId/cards/:cardId puts an owned card in the deck, and DELETE takes it out; both are edits of the deck, and answer it.
  • PUT /players/:playerId/decks/:deckId/types/:cardTypeKey with { "count" } (0 to 1,000; 0 takes the line out) sets how many of a type, and answers the deck.
  • A deck holds up to 200 lines (owned-limit). GET /players/:playerId/decks/:deckId/validation[?maxSize=] answers { "valid", "size", "problems": { "code", "message", "cardTypeKey"?, "cardId"? }[] } with code one of size, per-type-limit, not-owned, unknown-card-type, disabled-card-type.

Progression

  • GET /players/:playerId/xp answers a list of { "track", "xp" }. POST /players/:playerId/xp, server key, with an Idempotency-Key and { "amount", "track"? }, answers { "track", "xp" }, the new total (a retry answers the first total).
  • GET /players/:playerId/skill-trees answers a list of SkillTree, leaving hidden trees out for a player's token; GET /players/:playerId/skill-trees/:treeKey answers one SkillTree.
  • POST /players/:playerId/skill-nodes/:nodeKey/unlock, either, with { "realmId"?, "payWith"?, "allowances"? } (allowances from a server key only) answers { "nodeKey", "alreadyUnlocked", "charges": Movement[] }. Refusals: locked (with details.missing), insufficient-balance, allowance-exceeded, invalid-request for a consumed currency without its allowance, forbidden in a GreenMind game whose skill trees are unlocked by the game itself.
  • GET /players/:playerId/unlocks[?prefix=] answers a list of { "key", "source", "unlockedAt" }. PUT /players/:playerId/unlocks/:key, server key, with { "source"? } answers the unlock: 201 when this recorded it, 200 when it was recorded already, the first record standing. DELETE answers 204, whether or not the player held it.

Stats

  • GET /players/:playerId/stats[?realmId=] and GET /stats[?realmId=] answer a list of Stat, across every realm when no realm is named.
  • POST /players/:playerId/stats/:key/increment and POST /stats/:key/increment, server key, with { "by"?, "realmId"? } (by a whole number other than 0, 1 when absent, either sign; realmId null or absent for across every realm) answer Stat, the new value.

Definitions

Server key. Definitions are edited in the console; these read them, each a page at a time. Switched-off definitions are listed with enabled: false; deleted ones are not listed.

Route A list of
GET /definitions/realms Realm, open or not
GET /definitions/currencies { key, name, description, icon, scope, kind, max }
GET /definitions/item-types Item types, each with category, ownership, scope, switches, maxOwned, priceRule, prices and data
GET /definitions/card-types Card types, each with slug, perDeckLimit, starter, purchasable, priceRule, prices and data
GET /definitions/card-templates { key, name, description, enabled, data }
GET /definitions/skill-trees Skill trees, each node with its parents and prices

When an environment is full. Once an environment holds as many players as your plan allows (1,000 on the Free plan), signing in a player it has never seen is refused with 409 and conflict, and message says so. Players it already has keep signing in, and nothing is deleted. The console's Usage page marks the environment as full. To raise the cap, an admin of the org asks GreenMind for a higher plan with Request a higher plan on that page; deleting players you no longer need frees room in the meantime. See Getting started for how plans are asked for.

Errors

Every code a program may meet, and its status. The same list is in the OpenAPI document, as the Error schema's error.

error Status Meaning
invalid-request 400 The body is not JSON, or a field, query parameter, path parameter or header is missing, malformed, past a limit, or one the route does not take; details.field names it
invalid-ticket 400 The store refused the proof of identity a client sent
unauthorized 401 No credential, or one that is malformed, unknown, revoked or expired; a refresh token sent as an access token; a player signed out everywhere; a failed sign-in
forbidden 403 The credential is the wrong kind for the route, or a player's token sent what only a server may
service-disabled 403 The project has not switched on the service the route belongs to
project-not-approved 403 The project is waiting for GreenMind's review, was not approved, or was suspended: no credential of it is let in and no sign-in to it succeeds until GreenMind approves it
provider-disabled 403 The environment does not accept that way of signing in
quota-exceeded 403 A limit on how many of something there may be: players or saved bytes in an environment, a player's saves, a licence's seats. details.quota and details.limit say which
purchase-not-allowed 403 The store will not let this player buy
not-found 404 No such thing in this environment, or no such route. A thing of another environment is not-found too
conflict 409 The request clashes with the current state: a full lobby, a seat already taken, a status move that is not allowed, a save written by somebody else since
idempotency-conflict 409 The Idempotency-Key already names a different request
insufficient-balance 409 Not enough of a currency or a counted thing; details has what is held and what was asked
allowance-exceeded 409 A consumed currency would pass the allowance the game sent
balance-cap 409 A credit would take a balance past its currency's most
disabled 409 The definition it needs is switched off
not-purchasable 409 That is not for sale
owned-limit 409 The player already holds as many as the definition, or a deck, allows
locked 409 It needs unlocks or skill nodes the player does not have; details.missing lists them
not-usable 409 That item cannot be used or opened
already-consumed 409 Already used, opened or spent
no-current-realm 409 No realm is open now
no-steam-account 409 The player has proved no Steam account
already-active 409 The player already holds that product, and it cannot be held twice
order-in-progress 409 An order for that product is already waiting for the player
payload-too-large 413 The body is bigger than its route takes; details.limit is the most, in bytes
rate-limited 429 Too many requests: see Rate limits. Always with Retry-After
provider-refused 502 The store answered with an error of its own
unavailable 503 Something the route needs cannot be reached, or is not set up here. Retry later

The console answers three codes of its own, which the API never does: agreements-required, key-taken and invite-invalid.