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:

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

After you added purchases and published the game draft, send the request to enable purchases to games-partners@yandex-team.com. Be sure to include your game's name and ID in the email.

After you receive the confirmation from games-partners@yandex-team.com saying that the purchases are enabled, you can configure and test them.

Initialization

To allow users to make in-game purchases, use the payments object. Initialize it using the ysdk.getPayments() method.

Alert

In ysdk.getPayments(), you can pass an optional parameter signed: boolean, which is intended for protection against fraud. The choice depends on where the payments are processed:

  • If on the client side — call the method without the parameter or pass signed: false.

  • If on the server side — pass signed: true. In such cases, in the responses of the methods payments.getPurchases() and payments.purchase(), all data is returned only in encrypted form in the signature parameter.

Initialize with the default parameter (signed: false).

Example

var payments = null;
ysdk.getPayments()
    .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.
    });

Initialize with the parameter signed: true.

Example

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.
    });

 

Activating the purchase process

To activate an in-game purchase, use the method payments.purchase({ id, developerPayload }).

  • id: string — product ID set in the Games Console.
  • developerPayload: string — opptional 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.

Initialize with the default parameter (signed: false).

Returns Promise<Purchase>.

Purchase: object — includes purchase details. It contains the following properties:

  • productID: string — product ID.
  • purchaseToken: string — a token for consuming the purchase.
  • developerPayload: string — additional purchase data.

Initialize with the parameter signed: true.

Returns Promise<{ signature }>.

signature: string — encrypted 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

Use the payments.getPurchases() method to:

  • find out which purchases the player has already made;

  • check for unprocessed purchases;

  • handle consumable purchases.

Initialize with the default parameter (signed: false).

The method returns Promise<Purchase[]>.

Purchase[]: array — the list of purchases made by the player. Every Purchase contains the following properties:

  • productID: string — 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.
    });

Initialize with the parameter signed: true.

Returns Promise<{ signature }>.

signature: string — encrypted purchase data and the signature for player authentication.

Example

payments.getPurchases()
  .then(purchases => {
      fetch('https://your.game.server?purchase', {
          method: 'POST',
          headers: { 'Content-Type': 'text/plain' },
          body: purchases.signature
      });
  });

 

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[]: array — the list of products available to the user. This list is generated from the table in the Purchases tab of the Games Console. Each Product contains the following properties:

  • id: string — product ID.
  • title: string — product name.
  • description: string — product description.
  • imageURI: string — image URL.
  • price: string — product price in <price> <currency code> format.
  • priceValue: string — product price in <price> format.
  • priceCurrencyCode: string — currency code.

Product.getPriceCurrencyImage(size) — method to obtain the currency icon address.

  • size: string — optional parameter. Possible values:

    • small (by default) - getting a small icon.

    • medium — obtaining an icon of medium size.

    • svg — obtaining the icon in vector format.

The method returns string — the address of the icon, for example //yastatic.net/s3/games-static/static-data/images/payments/sdk/currency-icon-s@2x.png.

Example

var gameShop = []
payments.getCatalog()
    .then(products => {
        gameShop = products;
    });

Portal currency display

To display the name and icon of the portal currency in the game, use the SDK features:

  • Product.price: string — price with the currency code.
  • Product.priceCurrencyCode: string — currency code.
  • Product.getPriceCurrencyImage() returns string — URL of the currency icon.

For more details on the Product object, see Getting the catalog of all 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 might remain unprocessed. To avoid this, check for unprocessed purchases using the method payments.getPurchases() (e.g., each time the game is launched).

Alert

Before adding purchases, please configure unprocessed payments verification.

This verification is mandatory for passing moderation, 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.

Initialize with the default parameter (signed: false).

Example

payments.getPurchases().then(purchases => purchases.forEach(consumePurchase));

function consumePurchase(purchase) {
    if (purchase.productID === 'gold500') {
        player.incrementStats({ gold: 500 })
            .then(() => {
                payments.consumePurchase(purchase.purchaseToken)
            });
    }
}

Initialize with the parameter signed: true.

Example

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 potential in-game stat inflation, process purchases on the server side:

  1. Initialize ysdk.getPayments() with the parameter { signed: true }.
  2. Pass the signature received in the responses of payments.purchase() and payments.getPurchases() to your server, then decrypt it using your secret key.
  3. On your server, credit the player with the items earned in the game.
// 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)); // Verify the signature and credit the data to the player
    });

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=

Example of transmitted purchase data (in JSONformat)

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 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

id: string — 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.