HTTPS API
The four calls a game makes to Encore — plain JSON over HTTPS, no credential — for any engine or language.
A game reaches Encore at https://encore.plany.cloud with four POSTs. There is no
API key and no sign-in: the Encore is named by its encoreKey, which ships in the game,
and a purchase is proven by a Steam ticket.
The rest of the Plany API is gRPC, which a game client should not have to link, so
these four calls are plain JSON over HTTPS. The same calls are on the gRPC API as
plany.encore.v1.EncoreService's GetPublicFunding, ListPublicOffers,
BeginPurchase and CompletePurchase, with the same rules. The Unity,
Unreal and C++ SDKs make these
calls for you; each example below shows the SDK call beside the raw request.
Conventions
- Every call is
POSTwithContent-Type: application/json. - Bodies use protobuf's canonical JSON mapping: lowerCamelCase names, 64-bit integers as strings, enums by their full name.
- Money is
google.type.Money:currencyCode,units(a string) andnanos.4.99is{"currencyCode": "USD", "units": "4", "nanos": 990000000}. Never convert it through a float. - A request body is at most 64 KiB. Unknown fields are ignored.
- Responses carry
Cache-Control: no-store. A field with no value is still present, as its zero value ornull— except an optional one such asrunwayDays, which is absent when unset. - No
Authorizationheader is read, and sending one does nothing.
Get funding
POST /v1/public/funding:get
What the game may show its players about the servers' funding, shaped by the Encore's
funding_visibility.
curl -s https://encore.plany.cloud/v1/public/funding:get \ -H 'Content-Type: application/json' \ -d '{"encoreKey": "ek-01j9zq3m8k2v6c4e7w5t0y1r9a"}'The response:
{ "funded": true, "balance": null, "runwayDays": "37", "visibility": "FUNDING_VISIBILITY_RUNWAY", "purchasable": true, "unavailableReason": ""}| Field | Meaning |
|---|---|
funded | Whether the project has credit. Always present. |
balance | Only under FUNDING_VISIBILITY_EXACT_BALANCE; null otherwise. |
runwayDays | Whole days left at the last week's rate, under EXACT_BALANCE and RUNWAY. Absent when there is nothing to project from; "0" means less than a day. |
visibility | Which of the three the studio chose. |
purchasable, unavailableReason | Whether a player can buy right now, and if not, the reason — ENCORE_NOT_VERIFIED, ENCORE_NOT_READY, PUBLISHER_UNAVAILABLE or PUBLISHER_LIMIT_REACHED. |
If the funding cannot be read, the call fails — it never answers a zero balance that would read as "unfunded". Show nothing rather than a guess.
List offers
POST /v1/public/offers:list
The active offers the game may put in its store. Listed even before the Encore is
verified, with purchasable: false, so a store can be built and tested before launch.
curl -s https://encore.plany.cloud/v1/public/offers:list \ -H 'Content-Type: application/json' \ -d '{"encoreKey": "ek-01j9zq3m8k2v6c4e7w5t0y1r9a"}'The response:
{ "offers": [ { "offerId": "one-month", "displayName": "Keep the servers running — one month", "description": "Pays for about a month of servers for everybody.", "price": { "currencyCode": "USD", "units": "4", "nanos": 990000000 } } ], "purchasable": true, "unavailableReason": ""}A price is US dollars; Steam shows each player their own currency in the overlay. The credit an offer carries is not shown to players.
Begin a purchase
POST /v1/public/purchases:begin
Opens the Steam order. The Steam overlay appears for the player as this returns — possibly before the response reaches the game.
curl -s https://encore.plany.cloud/v1/public/purchases:begin \ -H 'Content-Type: application/json' \ -d '{ "encoreKey": "ek-01j9zq3m8k2v6c4e7w5t0y1r9a", "offerId": "one-month", "authTicket": "14000000a1b2c3…", "language": "de", "requestId": "6f1c2f0e-8a4b-4a39-9f4e-0b7d2c1a5e93" }'| Field | |
|---|---|
encoreKey | Required. |
offerId | Required. offerId from the list. |
authTicket | Required. The hex encoding of the ticket from ISteamUser::GetAuthTicketForWebApi("plany-encore"), once GetTicketForWebApiResponse_t has delivered it. The identity must be exactly plany-encore. |
language | ISO 639-1 for the overlay's text. Defaults to English. Steam's own names ("german") must be converted. |
requestId | Send one. A random UUID per purchase attempt, at most 128 characters. See below. |
The response:
{ "purchase": "01j9zr0c4v8t2m6k1x3p5w7n9q", "orderId": "4611686018427387904"}purchase is an opaque handle for the next call. orderId is Steam's order id — the
m_ulOrderID your MicroTxnAuthorizationResponse_t callback will carry. Match the
callback on it and ignore callbacks for any other order: Steam delivers every
order's answer to every listener.
Complete a purchase
POST /v1/public/purchases:complete
Tells Encore what the overlay said, and settles the order.
curl -s https://encore.plany.cloud/v1/public/purchases:complete \ -H 'Content-Type: application/json' \ -d '{ "encoreKey": "ek-01j9zq3m8k2v6c4e7w5t0y1r9a", "purchase": "01j9zr0c4v8t2m6k1x3p5w7n9q", "orderId": "4611686018427387904", "authorized": true }'The response:
{ "state": "STATE_CREDITED", "funding": { "funded": true, "balance": null, "runwayDays": "38", "visibility": "FUNDING_VISIBILITY_RUNWAY", "purchasable": true, "unavailableReason": "" }}authorizedism_bAuthorizedfrom the callback. It is a hint about when to ask Steam, never an answer: a decline is confirmed with Steam, and an approval credits only once Steam settles it.fundingis the funding after the purchase, so a store can update its runway without a second call. It isnullif it could not be read; the purchase has still happened.- Safe to call more than once, and safe not to call at all. Encore settles an approved order on its own within a couple of minutes if a game crashes first.
state | Do |
|---|---|
STATE_CREDITED | Thank the player. |
STATE_AUTHORIZED | Thank the player: they have paid, and Steam has not settled yet. Ask again in a couple of seconds to wait for CREDITED if you want to, and stop after about ten. |
STATE_INITIATED | After authorized: false, the decline is not confirmed with Steam yet. Treat it as cancelled. |
STATE_CANCELLED | The player declined. Not an error. |
STATE_FAILED | Steam did not complete it, and nothing was credited. |
Errors
A failure is a non-2xx status with a google.rpc.Status body:
{ "code": 9, "message": "this game has not been confirmed as end of life yet", "details": [ { "@type": "type.googleapis.com/google.rpc.PreconditionFailure", "violations": [ { "type": "ENCORE_NOT_VERIFIED", "subject": "ek-01j9zq3m8k2v6c4e7w5t0y1r9a", "description": "this game has not been confirmed as end of life yet" } ] }, { "@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "ENCORE_NOT_VERIFIED", "domain": "encore.plany.cloud", "metadata": {} } ]}Switch on the ErrorInfo reason (domain encore.plany.cloud). Every named refusal
carries one; the 400s also carry the same value as a PreconditionFailure violation
type. message is for logs, not for players.
| HTTP | code | Reason | Meaning |
|---|---|---|---|
| 400 | 9 | ENCORE_NOT_VERIFIED | Not verified, in review, declined or revoked. |
| 400 | 9 | ENCORE_NOT_READY | Verified, not yet transferred into its project. |
| 400 | 9 | PUBLISHER_UNAVAILABLE | The studio's Steam key is not working. |
| 400 | 9 | PUBLISHER_LIMIT_REACHED | The studio's outstanding limit would be exceeded. |
| 400 | 9 | OFFER_INACTIVE | The offer is off sale or does not exist. |
| 400 | 9 | ORDER_REFUSED | Steam would not open the order, or a retried attempt was already cancelled. |
| 401 | 16 | PLAYER_NOT_AUTHENTICATED | Steam did not vouch for the player. |
| 400 | 3 | — | A required field is missing, the body is not valid JSON, or a request id is too long. |
| 404 | 5 | — | No Encore has this key, or no such purchase for it. |
| 409 | 6 | — | A request id already used by a different player. |
| 429 | 8 | — | Rate limited. Nothing was done. |
| 503 | 14 | — | Steam is not answering. |
Every refusal of purchases:begin happens before the player is charged. None of
the named refusals improves by retrying in the next few seconds.
Retries
| Call | Retry on network failure, 429, 502, 503, 504? |
|---|---|
funding:get, offers:list | Yes, with backoff. |
purchases:complete | Yes, with backoff. It is idempotent. |
purchases:begin with a requestId | Yes, reusing the same requestId. A repeat returns the purchase the first attempt opened — same purchase, same orderId — rather than a second order. |
purchases:begin without a requestId | No. A timeout does not say whether an order was opened, and a second one is a second overlay a player could approve. |
A retried purchases:begin can hand back a purchase that already ended — the first
attempt reached Encore, and Steam then refused the order — as the refusal again rather
than as the purchase, so a game never waits for an overlay that is not coming.
A 429 is answered before any work is done. Limits apply per client address and, for
purchases:begin, per Encore key.
Rate limits and browser games
Encore limits by the address a request actually comes from, never by a header a caller writes, so players on different addresses never share a limit.
A browser game needs its origin allowed for CORS; without that no CORS headers are sent, which is right for every native client. Ask Plany to add your origin.
Testing before launch
Use a sandbox link and its Encore's key. Every call
behaves as it will for real, the Steam overlay appears, and nothing is charged or
credited — a sandbox purchase still ends in STATE_CREDITED, with nothing added to the
project's balance.