Документация

Каталог: игры, категории и товары

Каталог — это пять GET-эндпоинтов под /b2b/catalog. Все они только читают и все требуют ваш X-Api-Key. Чего у них нет — так это общего понятия «видно вам»: каждый применяет свой набор правил вашего аккаунта, и именно эта разница решает, на каком эндпоинте можно основывать решение о продаже.

ЭндпоинтСкрытая играСкрытие товараБлок типа доставкиРазрешённые регионыВаш allow-лист SKUSKU с картинкой
GET /gamesубираетсятолько счётчиктолько счётчиктолько счётчикнеттолько счётчик
GET /games/{slug}404нетнетнетнетнет
GET /games/{slug}/productsпустой 200дадададаисключены
GET /products/{id}404даданетнет404
GET /searchубираетсянетнетнетнетнет

«Только счётчик» означает, что правило применено к productCount — а значит и к inStockOnly, — но больше ни к чему в этом ответе. Колонка про allow-лист SKU важна, только если вашему аккаунту явно задан персональный список SKU; у большинства его нет, и для них эта колонка пуста по смыслу.

Колонка скрытой игры — единственное правило, которое отвечает в трёх разных формах, поэтому читайте её буквально, а не как «скрыта — значит 404». Скрытая для вашего аккаунта игра убирается из /games и из /search; она даёт 404 на /games/{slug} и 404 на /products/{id} для любого своего SKU; но на /games/{slug}/products это 200 с пустым массивом data. У этого эндпоинта вообще нет проверки на уровне игры — его единственный 404 это неизвестный или слитый слаг, — а скрытие доходит до него через фильтрацию списка товаров, которая для скрытой игры отсекает все строки. Поэтому пустой data здесь неоднозначен по устройству: «скрыто для вас» и «у этой игры действительно нет ничего, что вы можете заказать» приходят одними и теми же байтами. Когда их нужно различить, спросите /games/{slug}: 404 означает «скрыта», 200 — «видна и просто пуста для вас».

Прочитайте таблицу одной фразой: что вы реально можете заказать, говорит только эндпоинт товаров. Ближе всех к нему список игр — игра, весь остаток которой вам заблокирован, показывает 0 и выпадает из inStockOnly, — но счётчик это не SKU. Деталь игры применяет ровно одно правило, проверку скрытой игры: её список категорий для вас не фильтруется вовсе, поэтому категория может быть в ответе и не содержать ничего, что вам позволено заказать. Поиск применяет то же единственное правило. Попадание в любом из них означает «такая игра есть», а не «вы можете её продавать».

Три уровня

игра            PUBG Mobile                 slug: pubg-mobile
└── категория   UC (Global)                 id: 8812
    └── товар   PUBG Mobile 660 UC          id: 34521
УровеньЧем идентифицируетсяЧто это
Играчисловой id и стабильный slugТайтл, который ищет покупатель. Дедуплицирован между поставщиками: одна игра, сколько бы источников за ней ни стояло.
Категориячисловой idГруппировка внутри игры — обычно регион или семейство номиналов. Возвращается только внутри детали игры.
Товарчисловой idТот самый SKU, который вы заказываете: один номинал, одна цена, один тип доставки.

В URL игры адресуются по slug, товары — по id. Категории самостоятельно не адресуются: они нужны, чтобы фильтровать товары игры.

Список игр

GET /b2b/catalog/games?page=1&limit=200
X-Api-Key: gc_live_…
ПараметрПо умолчаниюПримечания
page1Нумерация с единицы.
limit50Ограничен сверху 200.
qПодстрока без учёта регистра по имени и slug.
inStockOnlytrueВыключает только точная строка false.
sortпо наличиюsort=name — по алфавиту; любое другое значение — по числу товаров, по убыванию.
typeСм. ловушку ниже.
{
"success": true,
"data": [
{ "id": 4412, "slug": "pubg-mobile", "name": "PUBG Mobile", "icon": "https://…", "type": "game", "productCount": 84 }
],
"pagination": { "page": 1, "limit": 200, "total": 1893 }
}

type как параметр и type как поле ответа — это два разных словаря. type=topup и type=cdkey выбирают по семейству исполнения, то есть по тому, как SKU доставляется у поставщика. Любое другое значение сопоставляется с собственным видом игры — тем, что вы видите в ответе: game, gift_card, software, dlc, currency, subscription, topup. То есть type=gift_card фильтрует по полю, а type=cdkey — нет.

productCount считается для вашего аккаунта, и inStockOnly опирается именно на него: игра, весь товар которой вне ваших регионов или скрыт для вас, покажет ноль и вовсе исчезнет из списка.

Игра и её категории

GET /b2b/catalog/games/pubg-mobile
{
"success": true,
"data": {
"id": 4412,
"slug": "pubg-mobile",
"name": "PUBG Mobile",
"icon": "https://…",
"description": "…",
"type": "game",
"categories": [
{ "id": 8812, "name": "UC (Global)", "slug": "uc-global" }
]
}
}

Неизвестный slug отдаёт 404 с телом { "success": false, "error": "Game not found" }. То же самое отдаёт slug игры, которую с тех пор смержили в другую, — редиректа нет намеренно: молчаливый 301 увёл бы ваш каталог на тайтл, который вы не выбирали продавать. Читайте 404 по ранее синхронизированному slug как «эту игру нужно перерезолвить», а не как «повторить позже».

Категории дедуплицируются по slug между источниками игры, поэтому пришедший id — один из нескольких эквивалентных категорий поставщиков. Используйте его только как значение фильтра, но не как собственный долговременный ключ.

И они не фильтруются под ваш аккаунт. 200 здесь означает только то, что игра от вас не скрыта: список категорий — это полный активный набор игры, поэтому категория может быть вам показана и не содержать ничего, что вам позволено заказать. Не стройте по этому ответу меню «что можно купить» — стройте его по различным категориям из эндпоинта товаров ниже.

Товары игры

GET /b2b/catalog/games/pubg-mobile/products?deliveryType=id_only&categoryId=8812
{
"success": true,
"data": [
{
"id": 34521,
"name": "PUBG Mobile 660 UC",
"slug": "pubg-mobile-660-uc",
"wholesalePrice": 1064.5,
"currency": "RUB",
"deliveryType": "id_only",
"region": "global",
"platform": null,
"amountType": { "type": "fixed", "value": 660 },
"inStock": true,
"deliveryDataSchema": [
{ "id": "gameUserId", "type": "text", "label": "Player ID", "required": true }
]
}
]
}

Три вещи, которых этот эндпоинт не делает, — и каждая кого-нибудь уже удивила:

  • Он не постраничный. Нет ни page, ни limit, ни блока pagination: вы получаете весь (уже отфильтрованный) список товаров игры. Рассчитывайте парсер на это.
  • Он не подбирает фильтры «примерно». categoryId и deliveryType сравниваются строгим равенством, а deliveryType ещё и регистрозависим: deliveryType=ID_ONLY не совпадёт ни с чем.
  • Он показывает не всё, что существует. SKU, чья форма доставки требует изображение или скриншот, полностью исключены из B2B-каталога — канала для файлов в этом API нет, а заказ по такому товару отклоняется. Здесь же применяются белый список регионов вашего аккаунта, блокировки типов доставки и скрытия отдельных товаров.

По deliveryType важна одна асимметрия: у ещё не классифицированного товара приходит deliveryType: null, и ?deliveryType=other его не вернёт — неклассифицированный SKU достижим только запросом без фильтра. Что из этого следует для ваших типов — на странице Прямое пополнение по API; все пять значений перечислены в Типах доставки.

Два поля читаются не так, как выглядят:

  • wholesalePrice — оптовая цена вашего аккаунта в рублях до скидки за объём. Скидка тарифа применяется только при создании заказа, к каждой его строке. Между этим ответом и тем моментом происходят две вещи, и тянут они в разные стороны: скидка тарифа цену опускает, а сама цена в момент заказа пересчитывается заново — по текущему курсу и с учётом изменений у поставщика. Поэтому это число — не потолок. При неизменившейся цене тариф может только опустить цену за единицу; если же цена успела вырасти, списанная сумма может оказаться выше той, что вы получили. Покупателю показывайте свою цену, это число используйте для маржи, а когда разница важна — перечитайте эндпоинт отдельного товара прямо перед заказом. Если цену для SKU посчитать не удалось, в этом списке придёт 0; считайте 0 не «бесплатно», а «сейчас заказать нельзя»: заказ по такому товару отбивается ошибкой ZERO_PRICE_ITEM.
  • inStock здесь всегда true. Эндпоинт возвращает только доступные товары, так что это константа, а не сигнал. Настоящий сигнал наличия — отсутствие товара в списке.

Отдельный товар

GET /b2b/catalog/products/34521

Возвращает тот же объект, что и элемент списка выше, с ценой, посчитанной для вашего аккаунта в момент запроса, — и, как и в списке, до скидки за объём. Это и есть вызов, который делают прямо перед созданием заказа: цены двигаются вместе с курсом и изменениями у поставщика, а списывается та цена, которая посчитана в момент оформления, а не та, что лежит у вас в кэше.

Нечисловой id — это 400 (Invalid product ID). Id, которого нет, который неактивен, который требует загрузки изображения, скрыт для вашего аккаунта или у которого тип доставки вами заблокирован, — это 404 (Product not found), намеренно один и тот же ответ: перебором id нельзя составить карту того, чего вашему аккаунту видеть не положено.

Одно правило этот эндпоинт не применяет — ваш список разрешённых регионов. Он проверяется в момент заказа, поэтому товар, который здесь вернулся с 200, при оформлении всё равно может быть отклонён с region_not_allowed. Если регион вас ограничивает, берите товары из списка товаров игры: там фильтр по региону работает.

GET /b2b/catalog/search?q=pubg&limit=10

q должен быть не короче 2 символов (иначе 400), limit по умолчанию 10 и ограничен сверху 20. Ищет он игры, а не товары, и предназначен для запроса «что это за игра», а не для синхронизации.

У поиска и детали игры одно и то же ограничение — оно видно в таблице в начале страницы: оба применяют только проверку скрытой игры. Список разрешённых регионов, блокировки типов доставки и скрытия на уровне товаров здесь не смотрятся: товары не возвращаются. Попадание в выдаче означает «такая игра есть», а не «вы можете её продавать»: перед показом покупателю читайте товары игры — это единственный эндпоинт, который отвечает на второй вопрос.

Парсите этот ответ защитно. В штатном режиме data — массив игр. Если поисковый бэкенд недоступен, эндпоинт деградирует до запроса в базу и возвращает data объектом с массивом games внутри. Клиент, рассчитанный на одну форму, сломается ровно в момент деградации.

Что свежее, а что кэшируется

Ни один каталожный ответ не несёт HTTP-заголовков кэширования, так что перед вами ничего не кэшируется. За нами:

ДанныеСвежесть
Список игрОтдаётся из снапшота, который перестраивается каждые 30 минут; под нагрузкой устаревший снапшот может отдаваться до 40 минут, пока идёт обновление.
Товары игры, отдельный товарЧитаются живьём на каждый запрос.
ЦеныСчитаются на каждый запрос, округление до двух знаков.
Правила видимости вашего аккаунта и сам ключКэшируются от 60 секунд до 5 минут после изменения на нашей стороне.

На практике: опубликованная нами игра появится у вас в пределах получаса, новый SKU внутри уже известной игры — сразу, а цене, полученной час назад, на чекауте доверять не стоит.

Что хранить у себя

Храните id. Названия переводят заново, цены двигаются с курсом, иконки перезаливают, категории мержат — id не меняются.

Схема, которая выживает в проде:

  • Метаданные игр держите у себя (id, slug, name, icon, productCount) и обновляйте раз в 15–30 минут. Это один небольшой постраничный вызов.
  • Товары игры читайте лениво, когда покупатель открыл эту игру, с коротким кэшем: 10–15 минут более чем достаточно.
  • Отдельный товар перечитывайте непосредственно перед POST /b2b/orders и цену берите оттуда.
  • Никогда не сохраняйте deliveryDataSchema как общую константу игры: схема живёт на уровне товара, и поставщики меняют имена полей между регионами и номиналами.

Обходить товары всех игр по расписанию можно, но расточительно: это тысячи вызовов против бюджета в 500 запросов в минуту — ради данных, которые в основном не изменились.

Перед запуском

  • Ключ связи с нашими данными — id товара; названия и цены остаются данными для отображения.
  • deliveryType в вашем типе объявлен nullable, а код фильтрации знает, что other не совпадает с null.
  • 404 по известному slug игры запускает перерезолв, а не цикл повторов.
  • wholesalePrice: 0 трактуется как «недоступно».
  • Ответ поиска разбирается в обеих формах.
  • Возможность продать решает эндпоинт товаров — никогда не попадание в поиске, не деталь игры и не список категорий.
  • Цена, с которой уходит чекаут, получена в ту же минуту, а не из ночной выгрузки, и цена покупателя не выводится из wholesalePrice так, будто это потолок.

Нужен ключ API?

Напишите, какие игры и регионы вы продаёте, — выдадим ключ и пройдём интеграцию вместе с вами. На вопросы по интеграции отвечают те же люди, которые ведут API.