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
- 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.
- 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. - 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.
- 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 toPOST /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. - Finish anything left over with
POST /commerce/orders/reconcile: an approval your game never reported, or one it reported just before it crashed. - Show the store from
GET /commerce/catalog?provider=steam, priced in what this player would pay. - Buy with
POST /commerce/ordersand{ "sku", "provider": "steam", "language": "en" }, which answers201with the order. Steam shows the approval dialog in the overlay. Send anIdempotency-Keyheader and a retried request answers the order the first one made. - Report the answer: when the overlay answers (
MicroTxnAuthorizationResponse_t), sendPOST /commerce/steam/authorizationswith{ "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:
GET /commerce/grantswith your server key answers a page of the grants not yet acknowledged, oldest first.- Credit each one, then acknowledge it with
POST /commerce/grants/:grantId/ack. An acknowledged grant is not answered again; acknowledging twice changes nothing. - 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.