GAPI Documentation

Массовые цены

Актуальные цены сразу по списку игр во всех регионах — один запрос на 1000 id

Массовые цены

Один запрос — до 1000 игр, по каждой цена в каждом активном регионе. Endpoint создан для сценария «у меня есть свой список id, мне нужны свежие цены по всем из них»: вместо тысячи запросов к /catalog/games/:gameId делается один.

Ответ отдаётся из того же кэша каталога, что и витрина сайта, поэтому 1000 id обрабатываются за ~0.2 секунды и стоят один запрос по rate limit.

POST /catalog/prices

Основная форма. Список id передаётся в теле JSON.

curl -X POST "https://gapi.qb2.ru/api/v1/catalog/prices" \
  -H "Authorization: Bearer gapi_xxxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{"ids": ["QERI8VMT", "18C11IFX", "diablo-iv"]}'

GET /catalog/prices

То же самое, но список идёт в query-строке через запятую. Удобно для быстрой проверки и для playground, но не для больших списков: длинный URL режется прокси и балансировщиками. Для списков длиннее ~100 id используйте POST.

curl -H "Authorization: Bearer gapi_xxxxxxxxxxxxxxxxxxxxx" \
  "https://gapi.qb2.ru/api/v1/catalog/prices?ids=QERI8VMT,diablo-iv&regions=UA,TR"

Параметры

Оба метода принимают одинаковый набор параметров: в POST — поля тела, в GET — параметры query-строки.

ПараметрТипОбязательныйОписание
idsstring[] / stringДаСписок id. В POST — массив, в GET — строка через запятую. Разделители: запятая, пробел, перевод строки, ;
platformenumНетauto (по умолчанию), psn, battlenet. См. Как распознаются id
regionsstringНетСписок кодов стран через запятую. По умолчанию — активные регионы каталога (UA,TR,PL,IN). all — все регионы, которые есть у игры
includestringНетДополнительные блоки через запятую: image, url, editions, meta, fx

Лимиты

ЧтоЗначение
Максимум id в запросе1000
Максимум id при include=editions200
Изданий на регион при include=editions12 (самые дешёвые)
Стоимость по rate limit1 запрос (независимо от количества id)
Размер ответа на 1000 id × 4 региона~1.7 МБ

Превышение лимита — это ошибка too_many_ids (400), а не молчаливое обрезание списка. Разбивайте большой список на части: при лимите 30 запросов в минуту это до 30 000 игр в минуту.

Как распознаются id

  • PSN — внутренний id из 8 символов (QERI8VMT). Регистр не важен.
  • Battle.net — slug (diablo-iv).

При platform=auto id сначала ищется среди PSN-игр, и только если не найден — среди Battle.net slug'ов. Явные platform=psn или platform=battlenet ограничивают поиск одной платформой.

Регионы и коды стран

PSN хранит коды стран в формате alpha-2 (UA, TR, PL, IN), Battle.net — в alpha-3 (UKR, TUR, POL, IND). Ключ региона в ответе всегда в «родном» для платформы формате, но:

  • фильтр regions понимает оба формата: regions=UA найдёт и UA у PSN, и UKR у Battle.net;
  • внутри каждого региона есть поле alpha2 — приводите к нему, если сравниваете цены между платформами.

По умолчанию отдаются только активные регионы каталога — те же четыре, что показывает сайт. У игр Battle.net регионов более 50, поэтому regions=all заметно увеличивает ответ.

Структура ответа

{
  "ok": true,
  "filters": { "platform": "auto", "regions": ["UA", "TR"], "include": [] },
  "counts": { "requested": 3, "resolved": 2, "notFound": 1 },
  "regions": ["TR", "TUR", "UA", "UKR"],
  "data": {
    "items": [
      {
        "id": "QERI8VMT",
        "platform": "psn",
        "title": "God of War Ragnarök",
        "regions": {
          "UA": {
            "alpha2": "UA",
            "currency": "UAH",
            "base": 259900,
            "current": 259900,
            "baseText": "2 599 ₴",
            "currentText": "2 599 ₴",
            "discountPercent": null,
            "discountLabel": null,
            "discountEndAt": null,
            "usd": 58.1344,
            "isFree": false,
            "isTrial": false,
            "isPsPlus": false,
            "edition": "Digital Deluxe Edition",
            "sku": "EP9000-PPSA08332_00-GOWRAGNAROKDELUX",
            "source": "graphql",
            "pricedAt": "2026-08-13T00:58:39.466Z",
            "variantsCount": 1
          },
          "TR": { "…": "то же самое для Турции" }
        },
        "regionsCount": 2,
        "cheapest": { "region": "UA", "usd": 58.1344, "text": "2 599 ₴" },
        "updatedAt": "2026-08-13T00:58:39.466Z"
      },
      {
        "id": "diablo-iv",
        "platform": "battlenet",
        "title": "Diablo® IV - Standard Edition",
        "regions": {
          "TUR": {
            "alpha2": "TR",
            "currency": "EUR",
            "base": null,
            "current": null,
            "baseText": "€49,99",
            "currentText": "€29,99",
            "discountPercent": 40,
            "usd": 34.5848,
            "…": "остальные поля"
          }
        },
        "regionsCount": 2,
        "cheapest": { "region": "TUR", "usd": 34.5848, "text": "€29,99" },
        "updatedAt": "2026-08-13T22:39:23.632Z"
      }
    ],
    "notFound": ["XXXXXXXX"]
  },
  "meta": { "…": "версия, requestId, rateLimit, курсы, indexBuiltAt" }
}

Верхний уровень

ПолеОписание
counts.requestedСколько уникальных id было в запросе (дубликаты схлопываются)
counts.resolvedСколько игр найдено
counts.notFoundСколько id не найдено
regionsВсе ключи регионов, встретившиеся в ответе
data.itemsНайденные игры, в порядке переданных id
data.notFoundНенайденные id — как их прислал клиент
meta.indexBuiltAtКогда был собран индекс каталога, из которого взяты цены

Игра

ПолеОписание
idКанонический id игры (для PSN — в верхнем регистре)
platformpsn или battlenet
titleНазвание. У нестандартных изданий содержит издание: «MLB The Show 25 — Digital Deluxe Edition»
regionsОбъект: код региона → цена
regionsCountКоличество регионов в ответе для этой игры
cheapestСамый дешёвый регион по курсу USD (null, если сравнивать нечего)
updatedAtСамое свежее pricedAt среди регионов

Цена в регионе

ПолеТипОписание
alpha2stringКод страны в alpha-2 — общий знаменатель для обеих платформ
currencystringКод валюты (UAH, TRY, PLN, INR, …)
baseinteger | nullЦена без скидки в сотых долях валюты: 259900 = 2599.00 ₴
currentinteger | nullАктуальная цена (со скидкой, если она есть), тоже в сотых
baseTextstringЦена без скидки, отформатированная под регион
currentTextstringАктуальная цена строкой. Для бесплатного — Бесплатно, для триала — Триал
discountPercentinteger | nullРазмер скидки в процентах
discountLabelstring | nullМетка скидки из стора (-40%, PS Plus)
discountEndAtstring | nullКогда заканчивается скидка (ISO-8601)
usdnumber | nullПересчёт актуальной цены в USD по текущему курсу
isFreebooleanЦена равна нулю
isTrialbooleanНоль относится к триалу/демо, а не к бесплатной игре
isPsPlusbooleanЦена привязана к подписке PS Plus
editionstring | nullИздание, к которому относится цена
skustring | nullИдентификатор товара в сторе (PSN)
sourcestring | nullИсточник цены (graphql — точные числовые данные)
pricedAtstringКогда цена была снята
variantsCountintegerСколько всего изданий у игры в этом регионе

base и current — целые числа в сотых долях валюты (как в сторе), а не рубли/гривны с копейками. Делите на 100. Если поле null, значит по этой строке пока есть только текстовая цена (baseText / currentText) — так бывает у Battle.net и у PSN-строк, до которых ещё не дошёл числовой парсер.

Дополнительные блоки (include)

ЗначениеЧто добавляет
imageimageUrl — обложка игры
urlstoreUrl и links у игры, storeUrl внутри каждого региона
metameta: издание (editionName), издатель, дата релиза, жанры, платформы, invariantName
editionseditions внутри каждого региона — до 12 изданий с ценами. Лимит запроса падает до 200 id
fxdata.exchangeRates — курсы валют, по которым считался usd
curl -X POST "https://gapi.qb2.ru/api/v1/catalog/prices" \
  -H "Authorization: Bearer gapi_xxxxxxxxxxxxxxxxxxxxx" \
  -H "content-type: application/json" \
  -d '{"ids": ["QERI8VMT"], "include": "image,url,meta,fx"}'

Примеры

const API_KEY = 'gapi_xxxxxxxxxxxxxxxxxxxxx';
const BASE = 'https://gapi.qb2.ru/api/v1';

// Разбиваем свой список на пачки по 1000 и собираем цены по всем регионам.
async function fetchPrices(ids) {
  const result = new Map();

  for (let i = 0; i < ids.length; i += 1000) {
    const chunk = ids.slice(i, i + 1000);

    const response = await fetch(`${BASE}/catalog/prices`, {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'content-type': 'application/json',
      },
      body: JSON.stringify({ ids: chunk }),
    });

    if (response.status === 429) {
      // Лимит: ждём до resetAt из заголовка и повторяем пачку.
      const resetAt = Date.parse(response.headers.get('X-RateLimit-Reset-Minute'));
      await new Promise((resolve) => setTimeout(resolve, Math.max(resetAt - Date.now(), 1000)));
      i -= 1000;
      continue;
    }

    const payload = await response.json();
    for (const item of payload.data.items) {
      result.set(item.id, item.regions);
    }
    if (payload.data.notFound.length) {
      console.warn('не найдены:', payload.data.notFound);
    }
  }

  return result;
}

const prices = await fetchPrices(['QERI8VMT', '18C11IFX', 'diablo-iv']);
const ragnarok = prices.get('QERI8VMT');

console.log(ragnarok.UA.current / 100, ragnarok.UA.currency); // 2599 UAH
console.log(ragnarok.TR.currentText);                          // ₺3.849
import requests

API_KEY = 'gapi_xxxxxxxxxxxxxxxxxxxxx'
BASE = 'https://gapi.qb2.ru/api/v1'


def fetch_prices(ids, regions=None, include=None):
    prices = {}

    for start in range(0, len(ids), 1000):
        chunk = ids[start:start + 1000]
        payload = {'ids': chunk}
        if regions:
            payload['regions'] = regions
        if include:
            payload['include'] = include

        response = requests.post(
            f'{BASE}/catalog/prices',
            headers={'Authorization': f'Bearer {API_KEY}'},
            json=payload,
            timeout=60,
        )
        response.raise_for_status()
        body = response.json()

        for item in body['data']['items']:
            prices[item['id']] = item

    return prices


prices = fetch_prices(['QERI8VMT', 'diablo-iv'], regions='UA,TR,PL,IN')

for game_id, item in prices.items():
    cheapest = item['cheapest']
    print(game_id, item['title'])
    for code, price in item['regions'].items():
        print(f"  {code}: {price['currentText']} (~${price['usd']})")
    if cheapest:
        print(f"  дешевле всего в {cheapest['region']}: {cheapest['text']}")
<?php
$apiKey = 'gapi_xxxxxxxxxxxxxxxxxxxxx';
$ids = ['QERI8VMT', '18C11IFX', 'diablo-iv'];

$ch = curl_init('https://gapi.qb2.ru/api/v1/catalog/prices');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $apiKey,
        'content-type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'ids' => $ids,
        'regions' => 'UA,TR,PL,IN',
    ]),
]);

$response = json_decode(curl_exec($ch), true);
curl_close($ch);

foreach ($response['data']['items'] as $item) {
    $ua = $item['regions']['UA'] ?? $item['regions']['UKR'] ?? null;
    printf("%s — %s\n", $item['title'], $ua['currentText'] ?? 'нет цены');
}

Ошибки

КодHTTPКогда
ids_required400Список ids пустой или не передан
too_many_ids400id больше лимита. В ответе есть limit и received
api_key_missing / api_key_invalid401Проблема с ключом
rate_limit_minute / rate_limit_day429Превышен лимит запросов
{
  "ok": false,
  "error": {
    "code": "too_many_ids",
    "message": "За один запрос можно передать не больше 1000 id. Разбейте список на части."
  },
  "limit": 1000,
  "received": 1500
}

Ненайденные id — это не ошибка: запрос возвращает 200, найденные игры лежат в data.items, а ненайденные — в data.notFound. Игра могла пропасть из каталога, а могла сменить gameId — он вычисляется из состава карточки и меняется, когда улучшается склейка изданий (см. Карточки игр). Такие id стоит перезапросить через поиск.

Практические сценарии

Слежение за ценами

Храните у себя updatedAt по каждой игре и опрашивайте endpoint по расписанию — изменение updatedAt или current означает, что цена в сторе поменялась. Цены пересобираются фоновым парсером, поэтому чаще раза в 10–15 минут опрашивать смысла нет.

Поиск лучшего региона

Поле cheapest уже содержит самый выгодный регион по курсу USD. Если нужна своя логика (например, с учётом комиссии), сравнивайте usd внутри regions самостоятельно — валюты у регионов разные, и напрямую current сравнивать нельзя.

Синхронизация каталога

  1. Один раз найдите id своих игр через /catalog/search.
  2. Сохраните их у себя.
  3. Дальше обновляйте цены только через /catalog/prices пачками по 1000.

On this page