Changelog
What has changed in the platform API, newest first. See Versioning for what each kind of change means for your client.
October 2026: GreenMind's review of new projects
- Every new project waits for GreenMind's review before it can use the platform. Until it is approved, every route that acts for one of its environments, and its sign-ins and crash uploads, answer the new refusal
403 project-not-approved, as they do while GreenMind has a project suspended. Projects that existed before are approved, so nothing a client already does changes. See Getting started. - The sessions socket closes with the new code
4006for such a project.
October 2026: custom dashboards
Added, so nothing a client already does changes:
- Dashboard tokens, a third credential, for a project's custom dashboard: a person signed in with GreenMind, in one environment, reaching only the routes that let a dashboard in, each with a permission checked on every call.
POST /auth/dashboard,GET /dashboard/sessionandGET /players, for dashboards.GET /config/values,GET /config/tables,GET /config/tables/:keyandGET /flagstake a dashboard token too, for whoever may open a dashboard in the environment.
October 2026: the sessions socket
Lobbies no longer need polling, and no game needs a realtime server of its own for them. See Realtime.
- The sessions socket: one WebSocket per player's client, following every session the player is seated in. It sends each change as it happens (
seat.joined,seat.left,seat.kicked,seat.promoted,seat.ready,session.status), with the session as it now stands; who is connected (presence); and your game's own messages between seats (relay), held to a size and rate per environment. Every frame is JSON in a versioned envelope,{ "v": 1, "type", "payload" }. GET /realtimeanswers where to open the socket, the protocol version, and the limits your plan sets.- Seats per session are set per project, in the console, up to what the org's plan allows; they were 4 for every project. Every session now answers its
seats, and a full one'sconflictcarriesdetails.limit. - Plans gained the socket's limits and the most seats a session may have: see Realtime.
October 2026: webhooks
- Webhooks: each environment can have endpoints its events are sent to as they happen, signed with a secret of each endpoint's own. Twelve events to start: player.created, player.deleted, purchase.settled, purchase.refunded, grant.created, crash_group.created, crash_group.regressed, config.promoted, session.ended, support_request.created, legal.accepted and webhook.test. Managed in the console under Live ops, Webhooks, with the new Webhooks permission (manage_webhooks). See Webhooks.
- GET /openapi.json describes each event in a webhooks section, its body as JSON Schema.
- Plans gain two limits: webhook endpoints per environment, and deliveries per environment per minute.
October 2026: an explorer, and headers a browser can read
- The API explorer reads the OpenAPI document and lists every route by area, with its credential, parameters, body schema, an example and its refusals, and sends any of them from your browser with a server key you paste or a test player the console makes outside
production. - A page's script on another site can read
X-RateLimit-Limit,X-RateLimit-Remaining,X-RateLimit-Reset,Retry-After,ETag,DeprecationandSunset: every answer names them inAccess-Control-Expose-Headers. Before, a browser hid them from scripts, so a web client could not see where it stood against the rate limits. - The GmPlatform sample, an Unreal Engine project that signs a player in with Epic Online Services and reads and writes through the plugin, is linked from Getting started.
October 2026: v1 settled
The platform API before its first release had grown three ways of refusing, four ways of paging and three ways of acknowledging. This settles v1 as one API. The API had not been released, so these changes were made without deprecation; from here on, v1 changes only as Versioning describes.
Described for programs
GET /openapi.json: the whole API as an OpenAPI 3.1 document, made from the same declarations that serve the routes.
One way to refuse
- Every refusal is
{ "error", "message", "details"? }, with one list of codes and their statuses (Errors). - A validation failure names its field in
details.field:body.<field>,query.<name>,path.<name>orheader.<name>. - A limit on how many of something there may be is
quota-exceeded(403), withdetails.quotaanddetails.limit, where it wasconflict: new players and saved bytes past the plan, a player's 5 MB of saves, a licence's seats; and in the console projects, environments, server keys and members. - A body over its route's limit is
payload-too-large(413), where it wasinvalid-request. - Something not set up or not reachable is
unavailable(503): crash reports or symbols with no bucket, a store that cannot be reached. This replacessymbols-not-configured,crash-reports-not-configuredand commerce'sprovider-unavailable. - The game services' and commerce's own codes are said the platform's way:
invalidanddata-too-largeareinvalid-request;unknown-providerisinvalid-requestnamingquery.providerorbody.provider;unknown-productandorder-not-foundarenot-found;idempotency-key-reusedisidempotency-conflict;too-many-ordersandtoo-many-requestsarerate-limited, withRetry-After. - A path the API does not have is a JSON
not-found.
One way to page
- Every list takes
limit(1 to 200, 50 by default) and an opaquecursor, and answers{ "items", "next" }. This replaces the blog'snextCursor(withtotal,categoryandtag), the ledger'sbefore, the commerce grants'after, and the crash symbols'truncated; and every list that had no paging, and named its items (configs,tables,flags,documents,tours,entitlements,inventories,decks,tracks,trees,unlocks,stats,realms,currencies,itemTypesand the rest), now has it. - The commerce changes feed is its own route,
GET /commerce/grants/changes, whosenextis nevernull. GET /crash-symbols/buildsisGET /crash-symbols/files: one item per file.- What one inventory holds is split by kind:
.../itemsand.../item-stacks,.../cardsand.../card-stacks, each with theinventorybeside its items. GET /realmslists the open realms;GET /realms/currentanswers the current one.
One way to answer
- A single thing is answered as itself:
GET /config/tables/:key,GET /players/:playerId/inventories/:inventoryandPOST /commerce/ordersno longer wrap theirs in{ "table" },{ "inventory" }and{ "order" }. - Something done with nothing to say answers
204: leaving a session, finishing a tour, deleting a player, a save slot, a deck or an unlock, releasing a licence. They answered{ "left": true },{ "completed": true },{ "deleted": ... }and{ "released": ... }. - Something made answers
201: a sign-in that makes the player, a save slot written for the first time, an unlock recorded for the first time, an order, and a support request or report, which now answers{ "id" }where it answered{ "filed": true }.
One way to name paths
| Was | Is |
|---|---|
POST /players/sign-in |
POST /auth/server |
GET /me, PATCH /me |
GET /players/me, PATCH /players/me |
GET /legal/outstanding |
GET /players/me/legal/outstanding |
POST /legal/accept |
POST /players/me/legal/accept |
GET /tours, POST /tours/:tourId/complete |
GET /players/me/tours, POST /players/me/tours/:tourId/complete |
GET /commerce/entitlements[?playerId=] |
GET /players/:playerId/entitlements |
POST /sessions/:id/kick, /promote { playerId } |
POST /sessions/:id/players/:playerId/kick, /promote |
POST /players/:playerId/items/use { itemId } |
POST /players/:playerId/items/:itemId/use |
POST /players/:playerId/items/use { itemTypeKey } |
POST /players/:playerId/item-stacks/use |
POST /players/:playerId/items/consume { itemId } |
POST /players/:playerId/items/:itemId/consume |
POST /players/:playerId/items/consume { itemTypeKey } |
POST /players/:playerId/item-stacks/consume |
POST /players/:playerId/cards/consume { cardId } |
POST /players/:playerId/cards/:cardId/consume |
GET /players/:playerId and PATCH /players/:playerId take a player's token as me, as every /players/:playerId route does.
Rate limits you can see
- Every answer carries
X-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset, and every429aRetry-After, including support filings, which promised one and did not send it. - The licence routes, the public key sets and requests with a credential that is not good are counted against the address they come from.
Signing out everywhere
POST /players/:playerId/sign-out, with a server key or asme, ends every token a player holds, refresh tokens included; and so does the console, from the player's page. A refresh token no longer outlives its player's sign-out.
Faster
- Server keys and players are checked against a short-lived cache, cleared at once when a key is revoked or a player deleted or signed out.
- Config values, tables and flags answer with an
ETag, and304while nothing has changed. - Crash uploads are read after the crash reporter is answered, by a worker, not by the API.