Commerce

Sell in your game through Steam, with your own Steam keys: products and their prices, orders from the Steam overlay to the charge, what each purchase gives a player, and the currency your server credits. Switch on the Commerce service for the project to use it.

Commerce is kept per environment, like everything else: your test environment has its own products, its own store credential (Steam's sandbox, where nobody is charged) and its own orders, apart from production's.

Setting up an environment

  1. In Steamworks, create a publisher Web API key with the Microtransactions and Sales Data permissions (Users & Permissions, Manage Groups, your group), and allow it from GreenMind's servers. Enable in-game purchases for your app.
  2. In the console, open the environment's Commerce, then Stores, and enter your Steam app id and the key. Leave Sandbox on everywhere but the environment your released game uses. Before anything is saved, Steam is asked whether the key covers the app (GetPartnerAppListForWebAPIKey): a key from another Steamworks group, a key Steam refuses, or Steam not answering refuses the save, and nothing changes; try again once Steam answers. The key is sealed as it is saved and never shown again; to change it, enter a new one. A change reaches every server within a minute.
  3. Add a product on the Products page: its sku (what your game asks for it by), what a purchase gives (its grants), and a Steam listing in the app of the environment's Steam credential (no other app is accepted, and none until the credential is saved), with an item id of your choosing and a USD price. Add prices in other currencies as Steam suggests them; a player whose currency has no price pays the USD one, which Steam converts.

A listing is unique per store app and item id across the platform, so a test environment selling in the same Steam app as production gives its products other item ids.

Apple's App Store, Google Play and Stripe are named on the Stores page for later: nothing can be sold through them yet.

What a purchase gives

Each product lists its grants, each multiplied by the line's quantity:

Grant Example Who holds it
entitlement-time { "entitlementKey": "season-pass", "durationSeconds": 2592000 } The player, on the platform
entitlement-permanent { "entitlementKey": "deluxe" } The player, on the platform
entitlement-count { "entitlementKey": "revives", "quantity": 5 } The player, on the platform
currency { "appKey": "<your app key>", "currency": "gems", "amount": 100 } See below

A product can be bought as often as a player likes, or one at a time: not while time it gave is still running, and not while another order for it is waiting. The catalog still lists it, with unavailableReason: "already-active", so your game can show it as owned.

Buying, from your game client

Every route here takes the player's access token.

  1. Prove the Steam account once per launch, after signing in: ask the Steam client for a Web API ticket with ISteamUser::GetAuthTicketForWebApi("gm-commerce"), and send its bytes as hex to POST /commerce/steam/identify. Purchases are made as that Steam account from then on. A player whose sign-in named a Steam identity is covered without it.
  2. Finish anything left over with POST /commerce/orders/reconcile: an approval your game never reported, or one it reported just before it crashed.
  3. Show the store from GET /commerce/catalog?provider=steam, priced in what this player would pay.
  4. Buy with POST /commerce/orders and { "sku", "provider": "steam", "language": "en" }, which answers 201 with the order. Steam shows the approval dialog in the overlay. Send an Idempotency-Key header and a retried request answers the order the first one made.
  5. Report the answer: when the overlay answers (MicroTxnAuthorizationResponse_t), send POST /commerce/steam/authorizations with { "steamOrderId", "authorized" }. The platform asks Steam itself and never takes your game's word: only an order Steam says was approved is charged and completed. The answer carries the order and what the player now holds.

GET /players/me/entitlements answers what the player holds at any time, a page at a time.

Currency

Kept by the platform. When the project has the Inventory service on and the environment defines the currency (a balance currency, on its Game data pages, whose key is the grant's currency), a purchase credits it straight to the player's balance, with a commerce.purchase line in their ledger naming the order. Nothing needs collecting, and those grants never appear in the list below. A refund takes back what the player still holds of it, with a commerce.refund line. A credit the balance cannot take (past the most the currency allows, or held per realm while no realm is open) leaves the order waiting until it can. See Game services.

Kept by your server. Any other currency is yours: the platform records it and never holds a balance. Your game server collects what was bought and credits it to its own wallet:

  1. GET /commerce/grants with your server key answers a page of the grants not yet acknowledged, oldest first.
  2. Credit each one, then acknowledge it with POST /commerce/grants/:grantId/ack. An acknowledged grant is not answered again; acknowledging twice changes nothing.
  3. Repeat until the list is empty. A server that was down collects what it missed when it is back; nothing is pushed to you.

Credit a grant and record its id in one transaction on your side, so a crash between crediting and acknowledging cannot credit it twice.

Refunds and chargebacks. Steam reports them, and they are acted on within minutes: time and counts come off the player's entitlements by themselves, and a currency grant is marked revoked (revokedAt, revokeReason). Follow every change with GET /commerce/grants/changes, a change feed: without a cursor it starts from the environment's first change, and each page answers the changes (a grant granted or revoked) in the order they happened, with next, the cursor to ask with next. Unlike any other list, next is never null: keep the last one you acted on, and ask again with it; an empty page means you are up to date. A grant revoked after it was granted comes back again, revoked, so take back what you can of each revoked one. A change never lands behind a cursor you already have, so nothing is skipped however the pages fall. Store the cursor as given; it means nothing to read. A grant revoked before you acknowledged it is still answered once: acknowledge it without crediting it.

Your server can read any of its players' entitlements with GET /players/:playerId/entitlements.

In the console

The environment's Commerce section has the products, every order with everything Steam said about it, what each player holds (and Give time, for a goodwill gesture or a purchase the store lost), the store reports and the stores. Everything there needs the Commerce permission.

Routes

Method Path Credential Answers
GET /commerce/catalog?provider=steam player { products }, priced for the player
POST /commerce/orders player 201 with the order
POST /commerce/steam/identify player { steamId }
POST /commerce/steam/authorizations player { order, entitlements }
POST /commerce/orders/reconcile player { orders, entitlements }
GET /players/:playerId/entitlements either A page of entitlements
GET /commerce/grants server A page of grants not yet acknowledged
GET /commerce/grants/changes server A page of every change, and where to ask next
POST /commerce/grants/:grantId/ack server The grant, acknowledged

A refusal from these routes is the platform's, as everywhere (see the API reference), with these codes particular to selling:

error Status When
invalid-request 400 provider is not steam (details.field names it), or another field is wrong
invalid-ticket 400 Steam refused the ticket: malformed, expired, or made for another app or identity
not-found 404 No such sku in this environment, or no such order of this player's
not-purchasable 409 Switched off, not listed on this store, no usable price, or a grant nothing can give
no-steam-account 409 The player has proved no Steam account
already-active 409 Bought one at a time, and its time is still running
order-in-progress 409 Bought one at a time, and another order for it is waiting
idempotency-conflict 409 The Idempotency-Key was used for another sku
purchase-not-allowed 403 Steam will not let this player buy
rate-limited 429 Ten orders started in the last hour, or ten identify calls in ten minutes; see Retry-After
provider-refused 502 Steam refused the order
unavailable 503 No Steam credential here, or Steam could not be reached or refused the key

An order that waits for the player is canceled after 30 minutes; one Steam never received is failed after 15.