In-game purchases
You can generate revenue by offering users the option to make in-game purchases. For example, they can buy extra time for completing a level or accessories for their character. To do this:
- Enable in-game purchases in the Yandex Games Console.
- Configure the SDK to work with purchases.
Portal currency
Yan is the portal currency of the Yandex Games platform. You can use them to pay for in-game purchases. Yans are stored on the player's balance, which is shared across all games and can be topped up with a bank card. The exchange rate of Yans to rubles is dynamic.
Note
For international payments, the exchange rate of one Yan to the national currency depends on the player's country.
Players can top up their balance:
- In the catalog header.
- In the player profile.
- When making an in-game purchase.
Users can also earn Yans as bonus rewards for participating in promos or purchasing fixed packages.
Both authorized and unauthorized Yandex users can make in-game purchases. Users can log in directly during the game, including at the time of making a purchase.
The introduction of the portal currency will not affect the terms and conditions for paying the licensing revenue to developers.
Conditions
After adding purchases and publishing the game draft, send a request to enable purchases to games-partners@yandex-team.ru. Be sure to include your game's name and identifier (ID) in the email.
After receiving a confirmation reply from games-partners@yandex-team.ru saying that the purchases have been enable, you'll be able to configure and test them out.
Initialization
To allow users to make in-game purchases, use the payments
object.
var payments = null;
ysdk.getPayments({ signed: true }).then(_payments => {
// Purchases are available.
payments = _payments;
}).catch(err => {
// Purchases are unavailable. Enable monetization in the Games Console.
// Make sure the Purchases tab in the Games Console features a table
// with at least one in-game product and the "Purchases are allowed" label.
})
signed: true
: Optional parameter. Returns the signature parameter in the methods payments.getPurchases()
and payments.purchase()
. It serves to protect the game from fraudulent activity.
Activating the purchase process
You can activate an in-game purchase using the following method:
payments.purchase({ id, developerPayload })
id
: String, the product ID set in the Games Console.developerPayload
: String, an optional parameter. Additional purchase data that you want to send to your server (transferred in the signature parameter).
The method opens a frame with a payment gateway. Returns Promise<Purchase>
.
Purchase
: Object, includes purchase details. It contains the following properties:
productID
: String, the product ID.purchaseToken
: String, a token for consuming the purchase.developerPayload
: String, additional purchase data.signature
: String, purchase data and the signature for player authentication.
After the player successfully makes a purchase, Promise
switches to the "resolved" state. If the player didn't make a purchase and closed the window, the Promise
state becomes "rejected".
Alert
Unstable internet connection may cause successful purchases not being registered by the game. To avoid this, use the methods described in the sections Checking for unprocessed purchases and Processing purchases and crediting in-game currency.
Failure to follow these instructions may result in the disabling of purchases in the app or in the app's depublishing.
If the player is not logged in to Yandex at the time of purchase, the log in window appears. You can also prompt the user to log in in advance.
Example
General:
payments.purchase({ id: 'gold500' }).then(purchase => {
// Purchase successful!
}).catch(err => {
// Purchase failed: no product with this id exists in the Games Console,
// the user didn't log in, changed their mind, and closed the payment window,
// the purchase timed out, there were insufficient funds, or for any other reason.
})
Using the optional developerPayload
parameter:
payments.purchase({ id: 'gold500', developerPayload: '{serverId:42}' }).then(purchase => {
// purchase.developerPayload === '{serverId:42}'
})
Getting a list of purchased items
To find out what purchases the player has already made, use the method:
payments.getPurchases()
The method returns Promise<Purchase[]>
.
Purchase[]
: Array, the list of purchases made by the player. The list has the following property:
signature
: String, purchase data and the signature for player authentication.
Each Purchase
purchase contains the following properties:
productID
: String, the product ID.purchaseToken
: String, a token for consuming the purchase.developerPayload
: String, additional purchase data.
Example
var SHOW_ADS = true;
payments.getPurchases().then(purchases => {
if (purchases.some(purchase => purchase.productID === 'disable_ads')) {
SHOW_ADS = false;
}
}).catch(err => {
// Throws the USER_NOT_AUTHORIZED exception for logged off users.
})
Getting the catalog of all products
To get a list of available purchases and their cost, use the payments.getCatalog()
method.
The method returns Promise<Product[]>
.
Product[]
: Object, the list of products available to the user. It is generated from the table on the Purchases tab of the Games Console. Each Product
contains the following properties:
id
: String, the product ID.title
: String, the product name.description
: String, the product description.imageURI
: String, the image URL.price
: String, the product price in the<price> <currency code>
format. For example, "25 YAN".priceValue
: String, the product price in the<price>
format. For example, "25".priceCurrencyCode
: String, the currency code ("YAN").
Product
also has a method for getting the currency icon address:
getPriceCurrencyImage(size: ECurrencyImageSize = ECurrencyImageSize.SMALL)
: String, the icon address. For example, //yastatic.net/s3/games-static/static-data/images/payments/sdk/currency-icon-s@2x.png
. You can specify medium
, small
, or svg
as the size
parameter value.
Note
To enter the product price, use one of the following options:
price
.priceValue
and an icon fromgetPriceCurrencyImage
.
Example
var gameShop = []
payments.getCatalog().then(products => {
gameShop = products;
});
Processing purchases and crediting in-game currency
There are two types of purchases: non-consumables (such as for disabling ads) and consumables (such as for buying in-game currency).
To process non-consumable purchases, use the method payments.getPurchases()
.
To process consumable purchases, use the method payments.consumePurchase()
.
payments.consumePurchase()
The method returns a Promise
in the "resolved" state (if the processing is successful) or "rejected" state (if an error occurs).
Alert
After calling the payments.consumePurchase()
method, the processed purchase is permanently deleted. To account for this, you should first modify the player data using the player.setStats()
or player.incrementStats()
method and then process the purchase.
Example
payments.purchase({ id: 'gold500' }).then(purchase => {
// Purchase successful!
// Adding 500 gold to the account and consuming the purchase.
addGold(500).then(() => payments.consumePurchase(purchase.purchaseToken));
});
function addGold(value) {
return player.incrementStats({ gold: value });
// To learn more, see Player data.
}
purchaseToken
: String, a token returned by the payments.purchase()
and payments.getPurchases()
methods.
Checking for unprocessed purchases
If the user loses internet connection when making an in-game purchase, or your server becomes unavailable, the purchase may remain unprocessed. To avoid this, check for unprocessed purchases using the method payments.getPurchases()
(e.g., each time the game is launched).
Example for a game whose data is stored on the Yandex server
payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));
function consumePurchase(purchase) {
if (purchase.productID === 'gold500') {
player.incrementStats({ gold: 500 }).then(() => {
payments.consumePurchase(purchase.purchaseToken)
});
}
}
Example for a game whose data is stored on the developer's server
payments.getPurchases().then(purchases => {
fetch('https://your.game.server?purchase', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: purchases.signature
});
});
Fraud prevention
To protect yourself from game metrics fraud, use the serverPurchase(signature)
function for processing purchases instead of player.setStats()
or player.incrementStats()
.
Alert
For the serverPurchase(signature)
function to work, you need to configure game data saving on the developer's server and initialize purchases with the signed: true
parameter.
// Make sure that purchases are initialized with the { signed: true } parameter
payments.purchase({ id: 'gold500' }).then(purchase => {
// Purchase successful!
// Adding 500 gold on the server...
serverPurchase(purchase.signature.slice(1)); // fail: Check the signature.
serverPurchase(purchase.signature); // ok: Purchase confirmed.
serverPurchase(purchase.signature); // fail: Make sure the purchase is unique.
});
function serverPurchase(signature) {
return fetch('https://your.game.server?purchase', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: signature
});
}
The signature
parameter of the request delivered to the server contains purchase data and the signature. It's two strings in base64
encoding:<signature>.<JSON with the purchase data>
.
Signature example
hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=
JSON
format)
Example of transmitted purchase data (in Note that the data format of the signature
parameter used in the serverPurchase(signature)
function is different from the format used in the payments.getPurchases()
method.
In the payments.getPurchases()
method, the signature
parameter contains an array of purchase objects in the data
field. In the serverPurchase(signature)
function, it's the purchase object.
{
"algorithm": "HMAC-SHA256",
"issuedAt": 1571233371,
"requestPayload": "qwe",
"data": {
"token": "d85ae0b1-9166-4fbb-bb38-6d2a4ca4416d",
"status": "waiting",
"errorCode": "",
"errorDescription": "",
"url": "https://yandex.ru/games/sdk/payments/trust-fake.html",
"product": {
"id": "noads",
"title": "No ads",
"description": "Disable ads in the game",
"price": {
"code": "YAN",
"value": "49"
},
"imagePrefix": "https://avatars.mds.yandex.net/get-games/1892995/2a0000016d1c1717bd7a0149ccadc86078a1/"
},
"developerPayload": "TEST DEVELOPER PAYLOAD"
}
}
Secret key example
t0p$ecret
The secret key for signature verification is unique for the game. It is generated automatically when creating purchases in the Games Console. You can find it under the purchase table.
Example of signature verification on the server
import hashlib
import hmac
import base64
import json
usedTokens = {}
key = 't0p$ecret' # Keep the key secret.
secret = bytes(key, 'utf-8')
signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0='
sign, data = signature.split('.')
message = base64.b64decode(data)
purchaseData = json.loads(message)
result = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
if result.decode('utf-8') == sign:
print('Signature check ok!')
if not purchaseData['data']['token'] in usedTokens:
usedTokens[purchaseData['data']['token']] = True; # Use database.
print('Double spend check ok!')
print('Apply purchase:', purchaseData['data']['product'])
# You can safely apply purchases here.
const crypto = require('crypto');
const usedTokens = {};
const key = 't0p$ecret'; // Keep the key secret.
const signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=';
const [sign, data] = signature.split('.');
const purchaseDataString = Buffer.from(data, 'base64').toString('utf8');
const hmac = crypto.createHmac('sha256', key);
hmac.update(purchaseDataString);
const purchaseData = JSON.parse(purchaseDataString);
if (sign === hmac.digest('base64')) {
console.log('Signature check ok!');
if (!usedTokens[purchaseData.data.token]) {
usedTokens[purchaseData.data.token] = true; // Use database.
console.log('Double spend check ok!');
console.log('Apply purchase:', purchaseData.data.product);
// You can safely apply purchases here.
}
}
Note
Our support team can help publish finished games or WebApps on Yandex Games. If you have any questions about development or testing, ask them in the Discord channel.
If you are facing an issue or have a question regarding the use of Yandex Games SDK, please contact support:
signed: true
: Optional parameter. Returns the signature
parameter in the methods payments.getPurchases()
and payments.purchase()
. It serves to protect the game from fraudulent activity.
signature
: String, purchase data and the signature for player authentication.
id
: String, the product ID set in the Games Console.
developerPayload
: String, optional parameter. Additional purchase data that you want to send to your server (transferred in the signature
parameter).
The signature
parameter in the request sent to the server contains the purchase data and the signature. It's two strings in base64
encoding:<signature>.<JSON with the purchase data>
.
purchaseToken
: String, a token returned by the payments.purchase()
and payments.getPurchases()
methods.