Массовые цены
Актуальные цены сразу по списку игр во всех регионах — один запрос на 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®ions=UA,TR"Параметры
Оба метода принимают одинаковый набор параметров: в POST — поля тела, в GET — параметры query-строки.
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
ids | string[] / string | Да | Список id. В POST — массив, в GET — строка через запятую. Разделители: запятая, пробел, перевод строки, ; |
platform | enum | Нет | auto (по умолчанию), psn, battlenet. См. Как распознаются id |
regions | string | Нет | Список кодов стран через запятую. По умолчанию — активные регионы каталога (UA,TR,PL,IN). all — все регионы, которые есть у игры |
include | string | Нет | Дополнительные блоки через запятую: image, url, editions, meta, fx |
Лимиты
| Что | Значение |
|---|---|
| Максимум id в запросе | 1000 |
Максимум id при include=editions | 200 |
Изданий на регион при include=editions | 12 (самые дешёвые) |
| Стоимость по rate limit | 1 запрос (независимо от количества 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 — в верхнем регистре) |
platform | psn или battlenet |
title | Название. У нестандартных изданий содержит издание: «MLB The Show 25 — Digital Deluxe Edition» |
regions | Объект: код региона → цена |
regionsCount | Количество регионов в ответе для этой игры |
cheapest | Самый дешёвый регион по курсу USD (null, если сравнивать нечего) |
updatedAt | Самое свежее pricedAt среди регионов |
Цена в регионе
| Поле | Тип | Описание |
|---|---|---|
alpha2 | string | Код страны в alpha-2 — общий знаменатель для обеих платформ |
currency | string | Код валюты (UAH, TRY, PLN, INR, …) |
base | integer | null | Цена без скидки в сотых долях валюты: 259900 = 2599.00 ₴ |
current | integer | null | Актуальная цена (со скидкой, если она есть), тоже в сотых |
baseText | string | Цена без скидки, отформатированная под регион |
currentText | string | Актуальная цена строкой. Для бесплатного — Бесплатно, для триала — Триал |
discountPercent | integer | null | Размер скидки в процентах |
discountLabel | string | null | Метка скидки из стора (-40%, PS Plus) |
discountEndAt | string | null | Когда заканчивается скидка (ISO-8601) |
usd | number | null | Пересчёт актуальной цены в USD по текущему курсу |
isFree | boolean | Цена равна нулю |
isTrial | boolean | Ноль относится к триалу/демо, а не к бесплатной игре |
isPsPlus | boolean | Цена привязана к подписке PS Plus |
edition | string | null | Издание, к которому относится цена |
sku | string | null | Идентификатор товара в сторе (PSN) |
source | string | null | Источник цены (graphql — точные числовые данные) |
pricedAt | string | Когда цена была снята |
variantsCount | integer | Сколько всего изданий у игры в этом регионе |
base и current — целые числа в сотых долях валюты (как в сторе), а не
рубли/гривны с копейками. Делите на 100. Если поле null, значит по этой строке
пока есть только текстовая цена (baseText / currentText) — так бывает у
Battle.net и у PSN-строк, до которых ещё не дошёл числовой парсер.
Дополнительные блоки (include)
| Значение | Что добавляет |
|---|---|
image | imageUrl — обложка игры |
url | storeUrl и links у игры, storeUrl внутри каждого региона |
meta | meta: издание (editionName), издатель, дата релиза, жанры, платформы, invariantName |
editions | editions внутри каждого региона — до 12 изданий с ценами. Лимит запроса падает до 200 id |
fx | data.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.849import 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_required | 400 | Список ids пустой или не передан |
too_many_ids | 400 | id больше лимита. В ответе есть limit и received |
api_key_missing / api_key_invalid | 401 | Проблема с ключом |
rate_limit_minute / rate_limit_day | 429 | Превышен лимит запросов |
{
"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 сравнивать нельзя.
Синхронизация каталога
- Один раз найдите id своих игр через
/catalog/search. - Сохраните их у себя.
- Дальше обновляйте цены только через
/catalog/pricesпачками по 1000.