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,:playerIdis alwaysme, and reaches only that player; naming any id, even their own, isforbidden. With a server key,:playerIdis a player's id, and a player of another environment isnot-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
POSTwith 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 with413 payload-too-largebefore 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-disabledwhen 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-approvedwhile 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.
Legal documents
Every legal route needs the legal service. See Other services for how a game uses them.
GET /legal/documents
Every legal document of the environment, ordered by key. Either credential. Answers 200 with a list of LegalDocument.
GET /players/:playerId/legal
Where one player stands. Answers 200 with LegalStanding, or not-found.
GET /players/:playerId/legal/outstanding
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 /realmsanswers a list of every open Realm, newest first;GET /realms/currentthe current one, ornot-foundwhile none is open.GET /players/:playerId/inventoriesanswers a list of every Inventory the player has.GET /players/:playerId/inventories/:inventoryanswers one.GET /players/:playerId/inventories/:inventory/balancesanswers a list of{ currencyKey, amount }, withinventorybeside it.GET /players/:playerId/inventories/:inventory/itemsanswers a list of the Items held one by one and not yet used, and.../item-stacksa list of the counted ones,{ itemTypeKey, quantity }; each withinventorybeside 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/deckswith{ "inventoryId", "name" }(name 1 to 60 characters after trimming) answers201with Deck;owned-limitpast 50 decks.GETandPATCH({ "name" }) on/players/:playerId/decks/:deckIdanswer Deck;DELETEanswers204.PUT /players/:playerId/decks/:deckId/cards/:cardIdputs an owned card in the deck, andDELETEtakes it out; both are edits of the deck, and answer it.PUT /players/:playerId/decks/:deckId/types/:cardTypeKeywith{ "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"? }[] }withcodeone ofsize,per-type-limit,not-owned,unknown-card-type,disabled-card-type.
Progression
GET /players/:playerId/xpanswers 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-treesanswers a list of SkillTree, leaving hidden trees out for a player's token;GET /players/:playerId/skill-trees/:treeKeyanswers one SkillTree.POST /players/:playerId/skill-nodes/:nodeKey/unlock, either, with{ "realmId"?, "payWith"?, "allowances"? }(allowancesfrom a server key only) answers{ "nodeKey", "alreadyUnlocked", "charges": Movement[] }. Refusals:locked(withdetails.missing),insufficient-balance,allowance-exceeded,invalid-requestfor a consumed currency without its allowance,forbiddenin 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:201when this recorded it,200when it was recorded already, the first record standing.DELETEanswers204, whether or not the player held it.
Stats
GET /players/:playerId/stats[?realmId=]andGET /stats[?realmId=]answer a list of Stat, across every realm when no realm is named.POST /players/:playerId/stats/:key/incrementandPOST /stats/:key/increment, server key, with{ "by"?, "realmId"? }(bya whole number other than 0, 1 when absent, either sign;realmIdnullor 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.