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 4006 for 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/session and GET /players, for dashboards.
  • GET /config/values, GET /config/tables, GET /config/tables/:key and GET /flags take 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 /realtime answers 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's conflict carries details.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, Deprecation and Sunset: every answer names them in Access-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> or header.<name>.
  • A limit on how many of something there may be is quota-exceeded (403), with details.quota and details.limit, where it was conflict: 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 was invalid-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 replaces symbols-not-configured, crash-reports-not-configured and commerce's provider-unavailable.
  • The game services' and commerce's own codes are said the platform's way: invalid and data-too-large are invalid-request; unknown-provider is invalid-request naming query.provider or body.provider; unknown-product and order-not-found are not-found; idempotency-key-reused is idempotency-conflict; too-many-orders and too-many-requests are rate-limited, with Retry-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 opaque cursor, and answers { "items", "next" }. This replaces the blog's nextCursor (with total, category and tag), the ledger's before, 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, itemTypes and the rest), now has it.
  • The commerce changes feed is its own route, GET /commerce/grants/changes, whose next is never null.
  • GET /crash-symbols/builds is GET /crash-symbols/files: one item per file.
  • What one inventory holds is split by kind: .../items and .../item-stacks, .../cards and .../card-stacks, each with the inventory beside its items.
  • GET /realms lists the open realms; GET /realms/current answers the current one.

One way to answer

  • A single thing is answered as itself: GET /config/tables/:key, GET /players/:playerId/inventories/:inventory and POST /commerce/orders no 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-Remaining and X-RateLimit-Reset, and every 429 a Retry-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 as me, 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, and 304 while nothing has changed.
  • Crash uploads are read after the crash reporter is answered, by a worker, not by the API.