In-app purchases

You can generate income by offering users the option to make in-game purchases. For example, extra time for completing a level or accessories for their character. To do this:

Alert

You can only test purchases after enabling their consumption. Otherwise, you might end up with unprocessed payments, making it impossible to pass moderation.

Conditions

Before working with the SDK, check the cooperation scheme. To do this, go to the Developer Console, navigate to the Account section, and check the value of the Unified licensing model field:

Enable monetization and purchases:

  1. Enable ad monetization. In the YAN partner interface, specify payment details for ads and purchases. After the data is verified, the contract status in the YAN interface in the Extras → Documents section will change to Offer accepted.

  2. Send an email requesting activation to games-partners@yandex-team.com. In the email, specify:

    • game name
    • game ID

    Tip

    Send the request as early as possible; you can do this before uploading the game archive or adding purchases.

  3. Wait for a confirmation email from games-partners@yandex-team.com stating that purchases are enabled.

For further steps, see Enable purchases.

Purchases are enabled automatically in all your games. Proceed to initialization.

Initialization

To enable players to make in-app purchases, use the payments object. You can:

  • Access ysdk.payments directly. Purchases are initialized upon the first call to any of the object's methods, which may cause the first call to be slightly slower.

  • Initialize the object using the ysdk.getPayments() method. This preloads the data required for payments methods, eliminating the delay during their first call.

Alert

Both YaGames.init() and ysdk.getPayments() accept an optional signed: boolean parameter for fraud protection. The value depends on where payments are processed:

  • For client-side processing — call the methods without the parameter or pass signed: false. Purchase methods will return data in plain text.

  • For server-side processing — pass signed: true. In this case, responses from payments.getPurchases() and payments.purchase() will return all data exclusively in encrypted form within the signature parameter.

Initialization with default parameter (signed: false).

Option 1: Simplified

1const ysdk = await YaGames.init();
2
3const payments = ysdk.payments;

Method 2: Preloading via getPayments()

1const ysdk = await YaGames.init();
2
3try {
4    const payments = await ysdk.getPayments();
5} catch (err) {
6    // Purchases unavailable.
7}

Initialization with signed: true.

Option 1: During SDK initialization

1const ysdk = await YaGames.init({ signed: true });
2
3const payments = ysdk.payments;

Option 2: Granular configuration via getPayments()

1const ysdk = await YaGames.init();
2
3try {
4    const payments = await ysdk.getPayments({ signed: true });
5} catch (err) {
6    // Purchases unavailable.
7}

 

Activating the purchase process

To activate an in-app purchase, use the payments.purchase() method. It opens a frame with a payment gateway.

Method signature:

1function purchase(data: {
2    id: string;
3    developerPayload?: string;
4}) => Promise<IPurchase | ISign> {}

Accepts parameters:

Parameter

Type

Description

id

string

Product ID that is set in the Developer Console.

developerPayload

string

Optional parameter. Contains additional information about the purchase that you want to pass to your server (will be passed in the signature parameter).

Initialize with the default parameter (signed: false).

Returns Promise<IPurchase> with purchase information.

1interface IPurchase {
2    productID: string;
3    purchaseToken: string;
4    developerPayload: string;
5}

Parameters:

Parameter

Type

Description

productID

string

Product ID.

purchaseToken

string

Token for consuming the purchase.

developerPayload

string

Additional information about the purchase.

Initialize with the parameter signed: true.

Returns Promise<ISign>.

1interface ISign {
2    signature: string;
3}

Parameter:

Parameter

Type

Description

signature

string

Encrypted purchase data and signature for verifying player authenticity.

After the player successfully makes a purchase, Promise resolves with fulfilled status. If the player didn't make a purchase and closed the window, Promise rejects with rejected status.

Alert

Unstable internet connection may lead to a situation where a player made a purchase, but it was not processed in the game. To avoid this, use the methods described in sections Checking for unprocessed purchases and payments.consumePurchase() to process purchases.

Failure to follow these instructions may result in the disabling of in-app purchases in the app or in the app's depublishing.

A user can make a purchase without authorization, but we recommend offering them to log in in advance or during the purchase.

Example

General:

1const ysdk = await YaGames.init();
2
3try {
4    const purchase = await ysdk.payments.purchase({ id: 'gold500' });
5} catch (err) {
6    // Purchase failed: no product with this id exists in the Developer Console,
7    // the user didn't log in, changed their mind and closed the payment window,
8    // the purchase timed out, there were insufficient funds, etc.
9}

Using the optional developerPayload parameter:

1const ysdk = await YaGames.init();
2
3try {
4    const purchase = await ysdk.payments.purchase({ id: 'gold500', developerPayload: '{serverId:42}' });
5} catch (err) {
6    // Handle purchase error.
7}

Getting a list of purchased items

Use the payments.getPurchases() method to:

  • Find out which purchases the player has already made.

  • Check for unprocessed purchases.

  • Handle non-consumable purchases.

Method signature:

function getPurchases(): Promise<IPurchase[] | ISign> {}

Initialize with the default parameter (signed: false).

Returns Promise<IPurchase[]> with an array of purchases. Each array element has the same format as the purchase returned by the payments.purchase() method.

Example

 1const ysdk = await YaGames.init();
 2
 3let SHOW_ADS = true;
 4
 5try {
 6    const purchases = await ysdk.payments.getPurchases();
 7
 8    if (purchases.some(purchase => purchase.productID === 'disable_ads')) {
 9        SHOW_ADS = false;
10    }
11} catch (err) {
12    // Error retrieving the list of purchases. Throws the exception PAYMENT_FAILURE.
13}

Initialize with the parameter signed: true.

Returns Promise<ISign>.

Parameter:

Parameter

Type

Description

signature

string

Encrypted purchase data and signature for player authenticity verification.

Example

 1const ysdk = await YaGames.init({ signed: true });
 2
 3try {
 4    const purchases = await ysdk.payments.getPurchases();
 5    // Send the list of purchases to the server.
 6    const response = await fetch('https://your.game.server/handlePurchases', {
 7        method: 'POST',
 8        headers: { 'Content-Type': 'text/plain' },
 9        body: purchases.signature
10    });
11} catch (err) {
12    // Error retrieving or processing the list of purchases.
13}

 

Getting the catalog of all products

To get a list of available purchases and their cost, use the payments.getCatalog() method.

Method signature:

 1interface IProduct {
 2    id: string;
 3    title: string;
 4    description: string;
 5    imageURI: string;
 6    price: string;
 7    priceValue: string;
 8    priceCurrencyCode: string;
 9    getPriceCurrencyImage(size: 'small' | 'medium' | 'svg'): string;
10}
11
12function getCatalog(): Promise<IProduct[]> {}

The method returns a list of products available to the user. Generated from the table in the In-app purchases tab of the Developer Console. Each IProduct contains properties:

Property

Type

Description

id

string

Product ID.

title

string

Product name.

description

string

Product description.

imageURI

string

Product image URL.

price

string

Product price in the format <price> <currency code>.

priceValue

string

Product price in the format <price>.

priceCurrencyCode

string

Currency code.

getPriceCurrencyImage(size)

string

Method for getting the currency icon address depending on the icon size parameter. Possible values:

  • small (default) — getting a small icon.

  • medium — getting a medium-sized icon.

  • svg — getting the icon in vector format.

Warning

Portal currency must be determined automatically (item 3.8). To do this, take its name and icon from IProduct properties. For more details, see Automatic detection of portal currency.

Example

 1const ysdk = await YaGames.init();
 2
 3let gameShop = [];
 4
 5try {
 6    const purchases = await ysdk.payments.getPurchases();
 7
 8    gameShop = purchases;
 9} catch (err) {
10    // Error retrieving the list of purchases.
11}

Processing purchases and crediting in-game currency

There are two types of purchases:

  • Non-consumable (e.g., disabling ads). Use the payments.getPurchases() method to process them.
  • Consumable (e.g., in-game currency). Use the payments.consumePurchase() method to process them.

payments.consumePurchase()

Alert

After calling the payments.consumePurchase() method, the processed purchase is permanently deleted. Therefore, first modify the player data using player.setData(), player.setStats() or player.incrementStats() methods, and then process the purchase.

Method signature:

function consumePurchase(purchaseToken: string): Promise<void> {}

Accepts purchaseToken returned by the payments.purchase() and payments.getPurchases() methods. If processing is successful, Promise resolves with fulfilled status; if an error occurs, it rejects with rejected status.

Example

 1const ysdk = await YaGames.init();
 2
 3function addGold(value) {
 4    return ysdk.player.incrementStats({ gold: value });
 5}
 6
 7try {
 8    const purchase = await ysdk.payments.purchase({ id: 'gold500' });
 9
10    await addGold(500);
11
12    await ysdk.payments.consumePurchase(purchase.purchaseToken);
13} catch (err) {
14    // Handle consumable purchase processing error.
15}

Checking for unprocessed purchases

Alert

This check is mandatory for passing moderation (item 1.13.1), so it's crucial to set it up even for test purchases. If you add purchases to the game and test them before configuring consumption, unprocessed payments could remain after the tests, making passing moderation impossible.

If the user loses internet connection when making an in-app purchase, or your server becomes unavailable, the purchase might remain unprocessed. To avoid this, check for unprocessed purchases using the payments.getPurchases() method, for example, each time the game is launched.

Initialize with the default parameter (signed: false).

Example

 1const ysdk = await YaGames.init();
 2
 3async function handlePurchase(purchase) {
 4    if (purchase.productID === 'gold500') {
 5        await ysdk.player.incrementStats({ gold: 500 });
 6
 7        await ysdk.payments.consumePurchase(purchase.purchaseToken);
 8    }
 9}
10
11const purchases = await ysdk.payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));
12
13for (let purchase of purchases) {
14    await handlePurchase(purchase);
15}

Initialize with the parameter signed: true.

Example

 1const ysdk = await YaGames.init({ signed: true });
 2
 3try {
 4    const purchases = await ysdk.payments.getPurchases();
 5    // Send the list of purchases to the server.
 6    const response = await fetch('https://your.game.server/handlePurchases', {
 7        method: 'POST',
 8        headers: { 'Content-Type': 'text/plain' },
 9        body: purchases.signature
10    });
11} catch (err) {
12    // Error retrieving or processing the list of purchases.
13}

 

Fraud prevention

To protect yourself from potential in-game stat inflation, process purchases on the server side:

  1. Initialize YaGames.init() or ysdk.getPayments() with the { signed: true } parameter.
  2. Pass the signature received in the responses of payments.purchase() and payments.getPurchases() to your server and decrypt it using the secret key.
  3. On your server, credit the player with the items earned in the game.
 1function serverPurchase(signature) {
 2    return fetch('https://your.game.server/handlePurchase', {
 3        method: 'POST',
 4        headers: { 'Content-Type': 'text/plain' },
 5        body: signature
 6    });
 7}
 8
 9// Make sure that purchases are initialized with the { signed: true } parameter.
10const ysdk = await YaGames.init({ signed: true });
11
12try {
13    const purchase = await ysdk.payments.purchase({ id: 'gold500' });
14
15    // Credit 500 gold on the server...
16    await serverPurchase(purchase.signature);
17} catch (err) {
18    // Purchase error.
19}

The signature parameter of the request sent 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=

Example of transmitted purchase data (in JSONformat)

Warning

The data format of the signature parameter in the serverPurchase(signature) function differs from that 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 a purchase object.

 1{
 2  "algorithm": "HMAC-SHA256",
 3  "issuedAt": 1571233371,
 4  "requestPayload": "qwe",
 5  "data": {
 6    "token": "d85ae0b1-9166-4fbb-bb38-6d2a4ca4416d",
 7    "status": "waiting",
 8    "errorCode": "",
 9    "errorDescription": "",
10    "url": "https://yandex.ru/games/sdk/payments/trust-fake.html",
11    "product": {
12      "id": "noads",
13      "title": "No ads",
14      "description": "Disable ads in the game",
15      "price": {
16        "code": "YAN",
17        "value": "49"
18      },
19      "imagePrefix": "https://avatars.mds.yandex.net/get-games/1892995/2a0000016d1c1717bd7a0149ccadc86078a1/"
20    },
21    "developerPayload": "TEST DEVELOPER PAYLOAD"
22  }
23}

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 Developer Console. The key is displayed on the In-app purchasesSettings tab.

Example of signature verification on the server

 1import hashlib
 2import hmac
 3import base64
 4import json
 5
 6usedTokens = {}
 7
 8key = 't0p$ecret' # Keep the key secret.
 9secret = bytes(key, 'utf-8')
10signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0='
11
12sign, data = signature.split('.')
13message = base64.b64decode(data)
14
15purchaseData = json.loads(message)
16result = base64.b64encode(hmac.new(secret, message, digestmod=hashlib.sha256).digest())
17if result.decode('utf-8') == sign:
18  print('Signature check ok!')
19
20  if not purchaseData['data']['token'] in usedTokens:
21    usedTokens[purchaseData['data']['token']] = True # Use database.
22    print('Double spend check ok!')
23
24    print('Apply purchase:', purchaseData['data']['product'])
25    # You can safely credit the purchase here.
 1const crypto = require('crypto');
 2
 3const usedTokens = {};
 4
 5const key = 't0p$ecret'; // Keep the key secret.
 6const signature = 'hQ8adIRJWD29Nep+0P36Z6edI5uzj6F3tddz6Dqgclk=.eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsImlzc3VlZEF0IjoxNTcxMjMzMzcxLCJyZXF1ZXN0UGF5bG9hZCI6InF3ZSIsImRhdGEiOnsidG9rZW4iOiJkODVhZTBiMS05MTY2LTRmYmItYmIzOC02ZDJhNGNhNDQxNmQiLCJzdGF0dXMiOiJ3YWl0aW5nIiwiZXJyb3JDb2RlIjoiIiwiZXJyb3JEZXNjcmlwdGlvbiI6IiIsInVybCI6Imh0dHBzOi8veWFuZGV4LnJ1L2dhbWVzL3Nkay9wYXltZW50cy90cnVzdC1mYWtlLmh0bWwiLCJwcm9kdWN0Ijp7ImlkIjoibm9hZHMiLCJ0aXRsZSI6ItCR0LXQtyDRgNC10LrQu9Cw0LzRiyIsImRlc2NyaXB0aW9uIjoi0J7RgtC60LvRjtGH0LjRgtGMINGA0LXQutC70LDQvNGDINCyINC40LPRgNC1IiwicHJpY2UiOnsiY29kZSI6IlJVUiIsInZhbHVlIjoiNDkifSwiaW1hZ2VQcmVmaXgiOiJodHRwczovL2F2YXRhcnMubWRzLnlhbmRleC5uZXQvZ2V0LWdhbWVzLzE4OTI5OTUvMmEwMDAwMDE2ZDFjMTcxN2JkN2EwMTQ5Y2NhZGM4NjA3OGExLyJ9fX0=';
 7
 8const [sign, data] = signature.split('.');
 9const purchaseDataString = Buffer.from(data, 'base64').toString('utf8');
10const hmac = crypto.createHmac('sha256', key);
11
12hmac.update(purchaseDataString);
13
14const purchaseData = JSON.parse(purchaseDataString);
15
16if (sign === hmac.digest('base64')) {
17  console.log('Signature check ok!');
18
19  if (!usedTokens[purchaseData.data.token]) {
20    usedTokens[purchaseData.data.token] = true; // Use database.
21    console.log('Double spend check ok!');
22
23    console.log('Apply purchase:', purchaseData.data.product);
24    // You can safely credit the purchase here.
25  }
26}

Note

Our support team can help publish finished games 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:

Write to chat

The signature parameter of the request sent to the server contains purchase data and a signature. It consists of two strings in base64 encoding: <signature>.<JSON with purchase data>.

  • Enable monetization.
  • In the Developer Console, go to the In-app purchases tab and make sure that:
    • There is a table with at least one in-game item.
    • The Purchases are enabled message is displayed.