Инап-покупки — единственный участок SDK, где ошибка в коде стоит игроку денег. Здесь недостаточно «работает у меня локально»: платёж проходит на стороне Яндекса, а начисление товара — на вашей, и между этими двумя событиями у игрока может отвалиться интернет, закрыться вкладка или разрядиться телефон. Вся глава, по сути, про один правильный порядок действий и про то, как подобрать покупки, которые остались висеть необработанными.
Материал проверен по официальной документации в сентябре 2026 года.
Что нужно сделать до первой строчки кода
Покупки не включаются галочкой в коде — их подключают на стороне платформы, и без этого игра не пройдёт модерацию. Порядок такой:
- Подключить покупки. В Консоли разработчика откройте раздел Профиль и посмотрите на поле Единая лицензионная схема. Если статус «Активен» — покупки во всех ваших играх включены автоматически. Если «На проверке» или «Подготовлен» — договор ещё оформляется, покупки подключатся сами, когда он вступит в силу. Если схема не подключена и вы не можете заключить ЕЛС (не РФ, не юрлицо и не ИП), документация описывает отдельный путь: подключить рекламную монетизацию, указать реквизиты в партнёрском интерфейсе РСЯ и написать запрос на почту поддержки партнёров с названием и ID игры. Отправляйте запрос как можно раньше — это самый долгий шаг.
- Добавить товары на вкладке Инап-покупки в Консоли.
- Обновить код игры — то, чем мы займёмся ниже.
- Протестировать под тестовым логином.
Поля товара в Консоли:
| Поле | Обязательно | Комментарий |
|---|---|---|
| ID | да | Строка, которую вы передадите в payments.purchase({ id }) |
| Название | да | На русском и английском, показывается в окне оплаты |
| Стоимость в янах | да | Целое число |
| USD / EUR | нет | Ориентировочные цены, рассчитываются автоматически, если указана одна из валют |
| Иконка | нет | 256 × 256 пикселей, PNG |
| Описание | нет | На русском и английском, до 200 символов |
Максимальное количество покупок в игре — 500. Список товаров в игре должен совпадать с Консолью: если на вкладке есть активные покупки, они должны быть в игре, и наоборот — при пустой вкладке в игре не должно быть магазина (пункт 1.13.6).
Важно
Тестировать покупки можно только после того, как в игре подключено консумирование. Иначе после тестов останутся необработанные платежи, и они сделают прохождение модерации невозможным (пункт 1.13.1). Сначала пишем обработчик, потом жмём «Купить» — даже в тестовом режиме.
Тестовый логин добавляется там же: Инап-покупки → Настройки → Список логинов для тестовых покупок. Деньги за покупки с этого аккаунта не списываются, доступ появляется через несколько минут после добавления.
Инициализация объекта payments
Есть два способа получить объект покупок, и оба валидны.
const ysdk = await YaGames.init();
// Способ 1: напрямую. Покупки инициализируются при первом вызове
// любого метода, поэтому первый вызов может быть чуть медленнее.
const payments = ysdk.payments;const ysdk = await YaGames.init();
// Способ 2: предзагрузка. Данные подтягиваются заранее,
// первый вызов метода не тормозит.
let payments = null;
try {
payments = await ysdk.getPayments();
} catch (err) {
// Покупки недоступны: не включена монетизация или нет активных товаров.
// Магазин в игре нужно просто не показывать.
}Обратите внимание на catch: getPayments() реально отклоняется, если покупки не подключены. Игра при этом должна продолжать работать — просто без витрины.
Параметр signed: boolean определяет, где вы обрабатываете платежи. Его можно передать либо в YaGames.init({ signed: true }), либо в ysdk.getPayments({ signed: true }).
| Где обрабатываете | Значение | Что возвращают purchase() и getPurchases() |
|---|---|---|
| На клиенте | без параметра или signed: false | Данные в открытом виде: IPurchase / IPurchase[] |
| На сервере | signed: true | Только { signature } — зашифрованные данные и подпись |
Дальше в главе основной сценарий — клиентский, он полностью рабочий и его достаточно для публикации. Серверную проверку разберём в конце.
Каталог товаров и портальная валюта
payments.getCatalog() возвращает список доступных игроку товаров, собранный из таблицы в Консоли.
interface IProduct {
id: string;
title: string;
description: string;
imageURI: string;
price: string; // «<цена> <код валюты>»
priceValue: string; // «<цена>»
priceCurrencyCode: string; // код валюты
getPriceCurrencyImage(size: 'small' | 'medium' | 'svg'): string;
}
function getCatalog(): Promise<IProduct[]> {}Размеры иконки валюты — small (по умолчанию), medium и svg. Значения large здесь нет, в отличие от аватарок игрока.
Именно на этом методе ломается больше всего игр. Пункт 1.13.2 требует, чтобы название и иконка портальной валюты определялись автоматически и брались из свойств IProduct. Причина простая: валюта зависит от региона игрока и может меняться на стороне платформы, а игра обязана показывать актуальную.
Частая ошибка
Нарисовать в спрайте «500 Yan» или подставить свою картинку монетки — прямой путь к отказу по пункту 1.13.2. То же самое относится к строке вида `${price} ян` в коде. Цена и валюта берутся только из IProduct, картинка валюты — только из getPriceCurrencyImage().
Рабочая витрина без сборки и фреймворков:
async function renderShop(container, payments) {
const products = await payments.getCatalog();
container.textContent = '';
for (const product of products) {
const card = document.createElement('button');
card.className = 'shop-card';
card.dataset.productId = product.id;
const icon = document.createElement('img');
icon.src = product.imageURI;
icon.alt = '';
const title = document.createElement('span');
title.className = 'shop-card__title';
title.textContent = product.title;
const description = document.createElement('span');
description.className = 'shop-card__description';
description.textContent = product.description;
// Цена: число из priceValue + иконка валюты из SDK.
const price = document.createElement('span');
price.className = 'shop-card__price';
price.textContent = product.priceValue;
const currency = document.createElement('img');
currency.className = 'shop-card__currency';
currency.src = product.getPriceCurrencyImage('svg');
currency.alt = product.priceCurrencyCode;
price.append(' ', currency);
card.append(icon, title, description, price);
container.append(card);
}
}alt у иконки валюты — это priceCurrencyCode: если картинка не загрузилась, игрок всё равно увидит код валюты, а не пустое место. Если вам нужна цена одной строкой, для этого есть готовое поле product.price в формате «цена + код валюты» — но иконка выглядит аккуратнее и точно так же соответствует требованию.
Покупка
function purchase(data: {
id: string;
developerPayload?: string;
}) => Promise<IPurchase | ISign> {}Метод открывает фрейм платёжного шлюза. Промис разрешается, если покупка совершена, и отклоняется, если игрок закрыл окно, передумал, не авторизовался, не хватило средств, истекло отведённое время или товара с таким id просто нет в Консоли. Все эти случаи приходят в один catch, поэтому не пишите в него «покупка не удалась, попробуйте позже» — чаще всего игрок просто передумал, и никакого сообщения показывать не нужно.
При signed: false возвращается:
interface IPurchase {
productID: string;
purchaseToken: string;
developerPayload: string;
}developerPayload — необязательная строка, которую вы кладёте в покупку и получаете обратно (а при signed: true — внутри подписи, на своём сервере). Удобно для сопоставления с внутренним идентификатором сессии или заказа.
Покупку можно совершить и без авторизации, но документация рекомендует предлагать вход заранее или в момент покупки. И тут есть жёсткое требование: для игр с покупками облачные сохранения обязательны (пункт 1.13.3) — прогресс и купленное должны быть доступны одному пользователю с разных устройств. Гость, купивший что-то и потерявший это при смене браузера, — повод для отказа.
Порядок действий: сначала начислить, потом консумировать
Покупки бывают двух типов, и обрабатываются они по-разному.
| Тип | Пример | Как обрабатывать |
|---|---|---|
| Постоянные | отключение рекламы, разблокировка режима | проверять наличие через getPurchases() при каждом запуске, не консумировать |
| Используемые | внутриигровая валюта, бустеры, жизни | начислить игроку, затем consumePurchase(token) |
function consumePurchase(purchaseToken: string): Promise<void> {}Осторожно
После вызова consumePurchase() обработанная покупка удаляется без возможности восстановления. Поэтому сначала модифицируйте данные игрока через player.setData(), player.setStats() или player.incrementStats() — и только потом консумируйте. Обратный порядок означает, что при сбое между двумя вызовами игрок заплатил и не получил ничего, а вы не сможете это исправить: токена больше нет.
Правильная последовательность для используемой покупки:
const ysdk = await YaGames.init();
const player = await ysdk.getPlayer();
async function buyGold500() {
const purchase = await ysdk.payments.purchase({ id: 'gold500' });
// 1. Сначала начисляем.
await player.incrementStats({ gold: 500 });
// 2. И только теперь консумируем.
await ysdk.payments.consumePurchase(purchase.purchaseToken);
}Если второй шаг упадёт, ничего страшного не произойдёт: покупка останется необработанной, и её подберёт код из следующего раздела. Единственный побочный эффект — игрок получит 500 монет дважды. Чтобы этого не было, начисление стоит делать идемпотентным: записывать в сохранение список уже обработанных purchaseToken и пропускать знакомые.
Дообработка необработанных покупок
Это обязательная для модерации часть (пункт 1.13.1). Если во время оплаты у игрока пропал интернет или ваш код не успел отработать, покупка останется в списке — и её нужно подобрать при следующем запуске.
const PROCESSED_KEY = 'processedPurchases';
async function processPendingPurchases(ysdk, player) {
let purchases;
try {
purchases = await ysdk.payments.getPurchases();
} catch (err) {
// Ошибка получения списка покупок (исключение PAYMENT_FAILURE).
// Не блокируем запуск игры — попробуем в следующий раз.
return { adsDisabled: false };
}
const saved = await player.getData([PROCESSED_KEY]);
const processed = new Set(saved[PROCESSED_KEY] || []);
// Постоянные покупки: только проверяем наличие, не консумируем.
const adsDisabled = purchases.some(p => p.productID === 'disable_ads');
for (const purchase of purchases) {
if (purchase.productID === 'disable_ads') continue;
if (processed.has(purchase.purchaseToken)) {
// Начисление уже было, осталось только консумировать.
await ysdk.payments.consumePurchase(purchase.purchaseToken);
continue;
}
const amount = { gold500: 500, gold1500: 1500 }[purchase.productID];
if (!amount) continue; // Неизвестный товар — оставляем как есть.
await player.incrementStats({ gold: amount });
processed.add(purchase.purchaseToken);
await player.setData({ [PROCESSED_KEY]: [...processed] }, true);
await ysdk.payments.consumePurchase(purchase.purchaseToken);
}
return { adsDisabled };
}Вызывайте эту функцию при каждом запуске игры — до того, как игрок увидит своё количество монет, и до того, как вы решите, показывать ли рекламу.
Пара замечаний по коду. flush: true во втором аргументе setData() здесь принципиален: мы хотим, чтобы отметка «начислено» ушла на сервер до консумирования, а не осталась в очереди. Список обработанных токенов имеет смысл подрезать — хранить, скажем, последние 50, чтобы не упереться в лимит 200 КБ на игрока. И, наконец, официальный пример в этом разделе документации написан неаккуратно (там смешаны forEach и последующий цикл for...of по значению, которого уже нет) — не копируйте его дословно, ориентируйтесь на смысл, а не на буквы.
Для постоянных покупок отдельно проверьте пункт 1.13.5: если игрок купил отключение рекламы, все рекламные показы должны исчезнуть — кроме rewarded video, которое игрок запускает сам, и стартового полноэкранного блока, который показывает сама платформа и который вы не контролируете. Реклама, продолжающая появляться после покупки, — причина отказа.
Обработка на сервере: signed
Если вы боитесь накруток, обрабатывайте покупки на своей стороне. Инициализируйте SDK с { signed: true }, отправьте полученную подпись на свой сервер и начисляйте товар там.
const ysdk = await YaGames.init({ signed: true });
try {
const purchase = await ysdk.payments.purchase({ id: 'gold500' });
await fetch('https://your.game.server/handlePurchase', {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: purchase.signature
});
} catch (err) {
// Ошибка покупки или её обработки.
}signature — две строки в base64 через точку: <подпись>.<JSON с данными о покупке>. Подпись проверяется алгоритмом HMAC-SHA256 с секретным ключом игры; ключ уникален для каждой игры, создаётся автоматически при добавлении покупок и лежит на вкладке Инап-покупки → Настройки. На сервере обязательно ведите базу использованных токенов — проверка подписи защищает от подделки, но не от повторной отправки одного и того же валидного платежа.
Один нюанс, на котором легко потерять час: формат данных отличается. В ответе purchase() подпись содержит объект покупки, в ответе getPurchases() — массив объектов в поле data.
Обратите внимание: любой внешний хост, к которому вы обращаетесь из игры, нужно заранее объявить и согласовать на вкладке CSP в Консоли. Без этого fetch на ваш сервер будет заблокирован.
Проверка перед отправкой на модерацию
Совет
Откройте игру с debug-панелью (URL игры + ?debug-mode=16) и включите переключатель 🪙 Currency mock. Валюта в вашем магазине должна смениться на «TST» с иконкой ¥. Если ничего не изменилось — значит, где-то остался хардкод, и пункт 1.13.2 не выполнен. Соседний переключатель 🐢 Network throttling имитирует плохой интернет — идеально, чтобы проверить, что покупка не теряется при таймауте.
Короткий чек-лист:
- Консумирование подключено и вызывается после начисления.
getPurchases()вызывается при каждом запуске игры, необработанные покупки подбираются.- Цена и иконка валюты берутся из
IProduct, а не зашиты в спрайты и строки. - Цена показана цифрами с указанием портальной валюты (пункт 1.13.4).
- Изображение, название и состав покупки в игре соответствуют тому, что игрок реально получает (пункт 1.13.5).
- Купленное сохраняется в облаке и доступно с другого устройства под тем же логином (пункт 1.13.3).
- Список товаров в игре совпадает с вкладкой Инап-покупки в Консоли (пункт 1.13.6).
- После покупки «отключить рекламу» реклама действительно пропала.
- В консоли браузера нет необработанных исключений от
purchase()при закрытии окна оплаты.
Что дальше
Покупки и реклама в играх Яндекса живут рядом: товар «отключить рекламу» напрямую влияет на то, какие блоки вы имеете право показывать, а rewarded video остаётся доступным даже после него. В следующей теме разберём рекламные методы SDK — полноэкранные блоки, видео с вознаграждением и sticky-баннер — и требования к моментам показа, по которым модерация отказывает чаще всего.