PlanyDocs
Encore

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 POST with Content-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) and nanos. 4.99 is {"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 or null — except an optional one such as runwayDays, which is absent when unset.
  • No Authorization header 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:

JSON
{
"funded": true,
"balance": null,
"runwayDays": "37",
"visibility": "FUNDING_VISIBILITY_RUNWAY",
"purchasable": true,
"unavailableReason": ""
}
FieldMeaning
fundedWhether the project has credit. Always present.
balanceOnly under FUNDING_VISIBILITY_EXACT_BALANCE; null otherwise.
runwayDaysWhole 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.
visibilityWhich of the three the studio chose.
purchasable, unavailableReasonWhether 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:

JSON
{
"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
encoreKeyRequired.
offerIdRequired. offerId from the list.
authTicketRequired. The hex encoding of the ticket from ISteamUser::GetAuthTicketForWebApi("plany-encore"), once GetTicketForWebApiResponse_t has delivered it. The identity must be exactly plany-encore.
languageISO 639-1 for the overlay's text. Defaults to English. Steam's own names ("german") must be converted.
requestIdSend one. A random UUID per purchase attempt, at most 128 characters. See below.

The response:

JSON
{
"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:

JSON
{
"state": "STATE_CREDITED",
"funding": {
"funded": true,
"balance": null,
"runwayDays": "38",
"visibility": "FUNDING_VISIBILITY_RUNWAY",
"purchasable": true,
"unavailableReason": ""
}
}
  • authorized is m_bAuthorized from 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.
  • funding is the funding after the purchase, so a store can update its runway without a second call. It is null if 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.
stateDo
STATE_CREDITEDThank the player.
STATE_AUTHORIZEDThank 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_INITIATEDAfter authorized: false, the decline is not confirmed with Steam yet. Treat it as cancelled.
STATE_CANCELLEDThe player declined. Not an error.
STATE_FAILEDSteam did not complete it, and nothing was credited.

Errors

A failure is a non-2xx status with a google.rpc.Status body:

JSON
{
"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.

HTTPcodeReasonMeaning
4009ENCORE_NOT_VERIFIEDNot verified, in review, declined or revoked.
4009ENCORE_NOT_READYVerified, not yet transferred into its project.
4009PUBLISHER_UNAVAILABLEThe studio's Steam key is not working.
4009PUBLISHER_LIMIT_REACHEDThe studio's outstanding limit would be exceeded.
4009OFFER_INACTIVEThe offer is off sale or does not exist.
4009ORDER_REFUSEDSteam would not open the order, or a retried attempt was already cancelled.
40116PLAYER_NOT_AUTHENTICATEDSteam did not vouch for the player.
4003—A required field is missing, the body is not valid JSON, or a request id is too long.
4045—No Encore has this key, or no such purchase for it.
4096—A request id already used by a different player.
4298—Rate limited. Nothing was done.
50314—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

CallRetry on network failure, 429, 502, 503, 504?
funding:get, offers:listYes, with backoff.
purchases:completeYes, with backoff. It is idempotent.
purchases:begin with a requestIdYes, 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 requestIdNo. 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.