Offers and purchases
What players can buy, what it credits, every state a purchase passes through, and every reason one is refused.
An offer is something a player can buy to fund the servers. A purchase is one player buying it.
The Encore key
Every Encore has an encore_key — ek-01j9… — and it is the only thing your game
needs to ship.
It is not a credential. It is in every copy of the game, and treat it that way: what it unlocks is reading the servers' public funding and starting a purchase, and anybody who owns the game may do both. It names no account, no project and no studio. The player is proven separately, by Steam, on every purchase.
Encore → your Encore → Integration → Encore keyOffers
Encore → your Encore → Offers → New offer → name, price and credit → Create offer| Field | Rule |
|---|---|
display_name | Required. What the player sees in the Steam overlay. At most 128 characters. |
price | US dollars, in whole cents, and positive. Steam shows each player the price in their own currency. |
credit | US dollars, positive, and at most the price less Steam's share (30%; the refusal names the exact ceiling). |
active | New offers are active. Set false to take one off sale. |
item_id | Output only. The item id sent to Steam, stable for the offer's life. |
Why the credit is capped
Steam pays you the price less its share, and you owe Plany the credit. A credit above the net would leave you owing more than you received on every sale, so it is refused:
| Price | Most it can credit, to the cent |
|---|---|
| $0.99 | $0.69 |
| $4.99 | $3.49 |
| $9.99 | $6.99 |
| $19.99 | $13.99 |
Crediting less than the most is allowed, and is how a studio covers its own costs or support. The gap is yours.
Changing and removing an offer
UpdateOffer(plany encore offers update) takesdisplay_name,description,price,creditandactive. Price and credit are checked together.- A price change applies to purchases begun afterwards. An order already open with Steam settles at the price and credit it was opened at.
DeleteOffer(plany encore offers delete) removes an offer nobody has bought. One with purchases is deactivated instead, because a purchase has to keep naming what was bought. Its id cannot be reused afterwards.
A game lists only active offers, and lists them even before the Encore is
verified — with purchasable: false — so you can build and test the store before
anybody can buy.
A purchase, step by step
- The game asks Steam for a ticket
ISteamUser::GetAuthTicketForWebApi("plany-encore"). The identity must be exactlyplany-encore; a ticket minted for anything else is refused. - The game calls BeginPurchase
With the Encore key, the offer id, the ticket and a fresh request id. Plany has Steam confirm the player with your key — so a ticket for another game proves nothing here — records the purchase, reserves its credit against your outstanding limit, and only then opens the order with Steam.
- The Steam overlay appears
Opened by Steam's servers, not by the game. The player approves or declines, and Steam delivers
MicroTxnAuthorizationResponse_tto the game. - The game calls CompletePurchase
With what the overlay said. Plany asks Steam where the order stands, settles an approved one, and credits the project only once Steam reports it settled.
The price, the credit and the project are never taken from any request. A game cannot say what the player paid; it can only say which offer they chose.
A crashed game still credits
CompletePurchase is safe to call more than once, and safe not to call at all.
Plany asks Steam about any order that has not moved for two minutes and settles it
on its own, so a game that crashes between the player approving and calling
CompletePurchase still funds the servers — usually within a couple of minutes.
A decline reported by the game is never believed on its own: Plany confirms it with Steam before cancelling. An order nobody ever answers is cancelled after an hour and its reservation released.
States
state | Final | Meaning |
|---|---|---|
STATE_INITIATED | Opened with Steam; the player has not answered, or declined and Steam has not confirmed it yet. | |
STATE_AUTHORIZED | The player approved. The order is settling with Steam, or the credit is being retried. The player has paid; thank them. | |
STATE_CREDITED | ✓ | Settled, and the project credited. ledger_entry names the entry in your ledger. |
STATE_CANCELLED | ✓ | The player declined, or never answered within an hour. Nothing was charged. |
STATE_FAILED | ✓ | Steam refused the order, had no record of it after an hour, or reversed it before it was credited. Nothing was credited. |
STATE_REVERSED | ✓ | Credited, then refunded or charged back, and the credit taken back. |
A sandbox purchase passes through the same states and credits nothing. It carries
sandbox: true.
Making a retry safe: request_id
Never retry BeginPurchase without a request id. It opens a Steam order, and a
timeout does not tell you whether it did. A blind retry can put a second overlay in
front of the player, and a player who approves both is charged twice.
Send a fresh random id — a UUID — with every purchase attempt, and reuse it only when retrying that same attempt:
- A second call with the same id, for the same Encore and the same player, returns the purchase the first call opened. No second order.
- The same id from a different player is refused with
ALREADY_EXISTS(HTTP 409). A game client chooses the id, and handing one player another's open order would disclose it. - A retry of an attempt whose order never opened gets the refusal again rather than the purchase, so a game does not wait for an overlay that is never coming.
- At most 128 characters.
The player pressing the button again is a new attempt and a new id. The SDKs do all of this for you.
Refusals
Every refusal of a purchase happens before the player is charged. A named refusal
carries its reason twice — as a google.rpc.ErrorInfo reason (domain
encore.plany.cloud) and, for the 400s, as a google.rpc.PreconditionFailure
violation type — so a client in any language can switch on it.
| Reason | HTTP | gRPC | When | What a game does |
|---|---|---|---|---|
ENCORE_NOT_VERIFIED | 400 | FAILED_PRECONDITION | The Encore is still being verified, is in review, was declined, or was revoked. | Hide the buy buttons. |
ENCORE_NOT_READY | 400 | FAILED_PRECONDITION | Verified, but not yet transferred into its project, so there is nothing to credit. | Same. It clears on its own. |
PUBLISHER_UNAVAILABLE | 400 | FAILED_PRECONDITION | The studio's Steam key is not working — usually revoked. Only the studio can fix it by rotating it. | Say the store is unavailable. |
PUBLISHER_LIMIT_REACHED | 400 | FAILED_PRECONDITION | The link's outstanding limit would be exceeded — or has not been set yet. Clears as the studio settles, or as other players' open purchases finish. | Try again later. |
OFFER_INACTIVE | 400 | FAILED_PRECONDITION | The offer was taken off sale, or does not exist. | Refresh the offers. |
ORDER_REFUSED | 400 | FAILED_PRECONDITION | Steam would not open the order — a wallet or region it will not sell in, say — or a retried attempt was already cancelled. | Say nothing was charged. |
PLAYER_NOT_AUTHENTICATED | 401 | UNAUTHENTICATED | Steam did not vouch for the player: no ticket, an expired one, one for another app or identity, or Steam offline. | Ask the player to be signed in to Steam. |
And the unnamed ones:
| HTTP | gRPC | When |
|---|---|---|
| 400 | INVALID_ARGUMENT | A required field is missing, or a request id is too long. |
| 404 | NOT_FOUND | No Encore has this key; or CompletePurchase named a purchase or order id that is not this Encore's. The two are one answer on purpose. |
| 409 | ALREADY_EXISTS | A request id reused by a different player. |
| 429 | RESOURCE_EXHAUSTED | Rate limited — per address, and on opening purchases per Encore key. Nothing was done. Back off and retry. |
| 503 | UNAVAILABLE | Steam is not answering. From BeginPurchase, nothing was charged; from CompletePurchase, the purchase will be settled on its own. |
None of the named refusals improves by retrying in the next few seconds.
Refunds and chargebacks
Steam refunds happen on Steam's side, sometimes days later. Plany reads each link's Steam order report every six hours:
- A refunded or charged-back order that was credited has its credit taken back
out of the project — in proportion when Steam itemises a partial refund, and in full
otherwise — and becomes
STATE_REVERSED. The project's ledger shows a refund, or a reversal for a chargeback. - An order reversed before it was credited becomes
STATE_FAILEDand credits nothing. - The project's balance can go negative. The credit may already have paid for hosting that genuinely ran. A negative balance keeps the servers asleep until it is topped up past zero.
- What you owe Plany on the link falls by the amount taken back.
Orders your game sold some other way, through its own backend, appear in the same Steam report and are ignored.
Reading purchases
Encore → your Encore → Funding → PurchasesNewest first. Each carries the offer, Steam's order_id, the player's SteamID64 in
player_id, the price and credit, the state and its message, and the times it was
credited or reversed.
roles/account.owner, roles/account.admin, roles/account.billing and
roles/account.viewer can all read purchases. Creating and changing offers is owner
and administrator.