Ключ передаётся в заголовке. Он привязан к проекту: каталог и цены вы получите именно те, что настроены в нём.
Заголовок
Authorization: Bearer gs_live_ваш_ключ
Ключ лежит в карточке проекта, там же его можно перевыпустить. Не кладите его в код фронтенда — запросы к нам делает только ваш сервер.
Подробности
Ограничения: 120 запросов в минуту на чтение и 20 в минуту на создание заказов, отдельно для каждого проекта. При превышении придёт 429 с заголовкомRetry-After— он говорит, через сколько секунд повторить. Заказы ограничены строже намеренно: цикл там тратит деньги, и в отличие от чтения это не отменить ожиданием.
Работает сейчас
Эти методы доступны и отвечают.
GET/ping
Проверка ключа
Первое, что стоит вызвать при подключении: подтверждает, что ключ рабочий, и возвращает аккаунт, к которому он привязан. Поле project — то же самое под старым именем, оставлено для уже написанных интеграций.
mode — live или test. Проверяйте его при старте: ключ, попавший не в тот конфиг, молчит ровно до момента, когда заказ либо не придёт, либо потратит настоящие деньги.
GET/catalog
Каталог
Только те товары, что включены в вашем проекте, и только те, что есть в наличии. Цены уже с вашей наценкой.
cost_* — сколько платите нам вы, price_* — то же с вашей наценкой, то есть цена для вашего покупателя. Пагинация идёт по has_more, а не по общему числу: считать точное количество на каждый запрос — лишняя нагрузка, а листать вперёд это не мешает. include=offers вкладывает номиналы прямо сюда — это ровно те же строки и те же цены, что отдаёт /catalog/{id}, читаются одним запросом на всю страницу вместо запроса на каждый товар. Витрине на 50 товаров это экономит 50 обращений. Учтите, что ответ становится заметно больше: если нужен только список названий и вилка цен, не просите.
GET/catalog/{id}
Товар и номиналы
Конкретные суммы пополнения или варианты ключа с ценой каждого. Товар, выключенный у вас в проекте, вернёт 404 — со стороны магазина его просто нет в каталоге.
cost — сколько вы платите нам. Розничную цену вы ставите у себя: поля price_* и price из каталога убраны 15.09.2026 вместе с настройкой наценки в кабинете — она всё равно ни на что у нас не влияла, а два места для одного числа расходились. stock: null означает, что поставщик не считает остаток по этой позиции — это не ноль. min_qty и max_qty — пределы поставщика на quantity; null значит «без ограничения», выход за них отвергается до списания денег. У пополнений (topups) quantity всегда равен 1 независимо от этих полей: количество задаётся выбором номинала. fields — что спросить у покупателя перед заказом. Все перечисленные обязательны. Ключи задаёт поставщик и они различаются от игры к игре — у 8 Ball Pool это user_id, у Age of Empire Mobile player_id плюс server_id, у AFK Journey account_id, — поэтому читайте их отсюда, а не зашивайте. Именно эти ключи уходят в объект fields при покупке; пустой список означает, что спрашивать нечего. У каждого поля есть type: "text" — свободный ввод, "select" — закрытый список, и тогда рядом лежит непустой options, а значение обязано быть ровно одним из options[].value (сервер поставщика отвергнет любое другое уже после списания). Поля type и options добавлены 15.09.2026; старые интеграции их просто не читают, ключи и label не менялись.
GET/balance
Баланс
Остаток на счёте аккаунта и последние движения по нему. Счёт один на все ваши проекты.
available — то, что реально можно потратить прямо сейчас: сравнивайте цену с ним, а не с balance. Обычно это одно и то же число, но если мы открыли вам овердрафт, overdraft показывает разрешённый минус, а balance после его использования становится отрицательным — это не ошибка ответа. Овердрафт по умолчанию нулевой, включается только по договорённости. order_id — заказ, за который списали: по нему можно найти потерянный заказ, если вебхук не дошёл. У пополнений и корректировок он null. Если у вас несколько проектов, движения приходят по всем: счёт общий. Заказ по чужому ключу при этом остаётся чужим — GET /orders/{id} отвечает только по заказам того проекта, чьим ключом вы спрашиваете.
GET/products
Что принимают свои товары
Границы сумм, доступные валюты и обязательные поля — те же самые, по которым заказ проверяется при создании. Нужен, чтобы форма не давала ввести то, что мы потом откажемся принять.
usd_rub — курс для вашей рублёвой витрины, тот же, что отдаёт GET /rates. Он НЕ равен currencies[].rate у steam_topup и не должен им подменяться: тот курс — делитель, которым сумма в рублях превращается в номинал, и счёт считается по нему. Границы Steam заданы в долларах номинала и пересчитаны по курсу поставщика на момент ответа. min округлён вверх, max вниз, так что значение, взятое отсюда, гарантированно пройдёт проверку. Кэшируйте на минуту — в ответе это поле cache_seconds, и то же самое приходит заголовком Cache-Control: private, max-age=60. Чаще незачем: курсы у поставщика мы перечитываем раз в две минуты (rates_refresh_seconds), так что минутный кэш не сделает вас устаревшее нас. Лимит метода — 120 запросов в минуту на проект, отдельный от лимита /quote. limit_scope: order — тысяча долларов это предел на один заказ; ограничений на сутки или на проект нет, потолком служит баланс. available: false у steam_topup означает, что поставщик или его курсы сейчас недоступны — заказы по нему не примутся.
GET/rates
Курс для рублёвой витрины
Сколько рублей стоит доллар, если считать по нему розницу. Наши цены, балансы и пополнения — в долларах; этот курс нужен, чтобы перевести их в рубли у себя.
Это НЕ тот курс, что лежит в /products → steam_topup → currencies[].rate. Тот — делитель, которым сумма в рублях превращается в долларовый номинал, и счёт за пополнение Steam считается именно по нему: возьмёте для номинала этот курс — и посчитанная вами сумма разойдётся с нашей, а значения у краёв диапазона будут отклонены. Здесь — курс для витрины: по нему считайте свою розницу, он учитывает стоимость конвертации рублей в доллары. Кэшируйте на минуту (cache_seconds, тот же Cache-Control: private, max-age=60), лимит общий с чтением — 120 запросов в минуту. То же число дублируется в /products полем usd_rub, чтобы не делать второй запрос, если вы и так туда ходите.
POST/recipient
Кто получит подарок
Имя и аватар владельца username в Telegram — то же, что показывает форма Fragment. Плюс ответ на вопрос, можно ли этому человеку подарить выбранное.
Параметры
product
telegram_stars или telegram_premium
username
Получатель, без @
months
Для Premium: 3, 6 или 12
quantity
Для звёзд: сколько. На то, кто получатель, не влияет
Три поля, три разных вопроса. found — нашли ли человека; can_receive — можно ли выдать ему именно это; reason — что это значит для получателя; resolution — что ответил источник в этой попытке. Решение о продаже принимайте по can_receive, карточку рисуйте по found, текст покупателю пишите по reason, а resolution берите только в логи. reason: found; has_premium — Premium у него уже есть; not_resolvable — за этим ником никого не удалось получить; unavailable — отказ по другой причине; unknown — источник не ответил, found и can_receive придут null, заказ создавать можно. Мы НЕ отдаём «такого пользователя нет» и не будем: снаружи отсутствующий аккаунт и аккаунт, который Fragment не отдаёт, — это один и тот же ответ, и @durov тому пример. resolution: fragment_ok — ник разрешён; fragment_said_no — Fragment заявил, что такого ника нет (иногда ошибочно); fragment_declined — отказал без объяснения; fragment_silent — не ответил. Для steam_topup — supplier_ok / supplier_said_no / supplier_silent. Для Steam передавайте product: steam_topup и steam_login вместо username; reason там cannot_refill, если Steam не примет пополнение. Показывайте имя и аватар покупателю до оплаты: это снимает большую часть ошибок в username. photo — прямая ссылка на CDN Telegram, она может смениться, не кэшируйте её надолго. Ответ кэшируется у нас на минуту. Лимит общий с /quote — 60 запросов в минуту.
POST/quote
Цена без покупки
То же тело, что у покупки, но ничего не создаётся и не списывается. Нужен, чтобы посчитать свою цену покупателю до заказа: у звёзд, Premium и пополнения Steam цена берётся из живых курсов, и в каталоге её нет.
Параметры
offer
Товар каталога: id номинала из GET /catalog/{id}
product
Свои товары: telegram_stars, telegram_premium или steam_topup
quantity
Для звёзд и каталога — количество
months
Для Premium: 3, 6 или 12
username
Для Premium: получатель. Без него цена ориентировочная — см. estimate
steam_login
Для Steam не нужен: цена от суммы, а не от аккаунта
estimate: true бывает только у Premium: цену за конкретного получателя называет Fragment, и когда её получить не удалось — имя не передали или Fragment не ответил — в ответе стоит наша сохранённая база, то есть ориентир, а не обещание. Передавайте username: тогда при estimate: false цена ровно та, что спишется при заказе, в ответе появляется recipient с именем и аватаром получателя, а если подарить нельзя — придёт 422 recipient_refused с причиной и с тем же recipient внутри объекта error, ещё до того как вы возьмёте деньги с покупателя. Отдельный запрос к POST /recipient при этом не нужен: у Premium имя и аватар приходят вместе с ценой, у звёзд — одним кэшируемым запросом. Лимит у метода свой: 60 запросов в минуту, отдельно от лимита заказов.
POST/orders
Покупка
Создаёт заказ и сразу списывает деньги с баланса. Выдача идёт в фоне — статус смотрите следующим методом.
Параметры
offer
Товар каталога: id номинала из GET /catalog/{id}
fields
Для пополнений: данные покупателя из поля fields того же ответа
product
Свои товары: telegram_stars, telegram_premium или steam_topup
username
Для Telegram: получатель, без @
quantity
Для звёзд — сколько штук, минимум 50; для каталога — сколько единиц
months
Для Premium: 3, 6 или 12
steam_login
Для Steam: логин аккаунта, латиницей
amount
Для Steam: сумма зачисления на аккаунт
currency
Для Steam: валюта суммы, например RUB или KZT
client_ref
Ваш номер заказа, обязателен. Повтор с тем же значением вернёт прежний заказ, а не купит второй
Обязательно передавайте client_ref. Если запрос отвалится по таймауту, повтор с тем же значением вернёт тот же заказ и не купит второй раз — ответ будет 200 вместо 201.
POST/orders
Пополнение Steam
Тот же метод, но сумму называете вы: amount — сколько зачислить на аккаунт, currency — в какой валюте. Цена считается по курсу на момент заказа.
Логин, а не отображаемое имя: деньги уходят на тот аккаунт, который назвали, и вернуть их нельзя. client_ref обязателен.
GET/orders
Список заказов
Ваши заказы, новые сверху. Нужен, чтобы сверить день, чтобы найти заказ, id которого вы не сохранили, — и чтобы следить за выдачей, если вебхук вам не подходит.
Параметры
status
processing, delivered или failed
client_ref
Ваш номер заказа. Точное совпадение — вернётся не больше одного
created_from
С какой даты: YYYY-MM-DD или YYYY-MM-DD HH:MM:SS, UTC
created_to
По какую. Дата без времени включает весь день
updated_since
Что изменилось с этого момента, старые изменения первыми. Так следят за выдачей без вебхука
page
Страница, с 1
per_page
Сколько на странице: по умолчанию 50, максимум 100
Коды выданных товаров в списке НЕ приходят — как и данные покупателя из fields. Нашли нужный заказ, забрали коды через GET /orders/{id}: так один утёкший ключ не выгружает всю историю кодов одним запросом. has_more — это не общее число: при активной торговле страницы съезжают, поэтому для сверки надёжнее сужать created_from и created_to, чем листать вглубь.
GET/orders/{id}
Статус заказа
Три состояния: processing — в работе, delivered — выдано, failed — не получилось, деньги вернулись на баланс. Здесь же приезжает сам товар: у ключей и подарочных карт коды лежат в delivery.items.
delivery появляется только у товаров каталога и только когда status = delivered: до выдачи там null, а не пустой список. В items столько кодов, сколько прислал поставщик — обычно по одному на единицу quantity, но сверяйте с длиной массива, а не считайте по quantity. У пополнений (topups) кодов нет вовсе, там items пустой: товар ушёл прямо на игровой аккаунт из fields. Для звёзд, Premium и Steam полей item, fields и delivery нет — это не каталог.
Песочница
Второй ключ, gs_test_…, к тому же проекту. Тот же каталог, те же цены, те же коды ошибок — но игровые деньги, и ни одного обращения к поставщику или к Fragment.
Режим решает ключ, а не запрос.
Поле в теле, которое отменяет списание, — слишком дешёвая вещь, чтобы на ней держалась граница. Какой ключ прислали, такой и режим; подделать нельзя.
Миры не пересекаются.
Тестовый ключ не видит боевых заказов и не может ими управлять, боевой не видит тестовых. Один и тот же client_ref можно прогнать сначала в песочнице, потом в бою.
Свой адрес вебхука и свой секрет.
Задаются отдельно. Если указать один адрес на оба режима, боевой приёмник не сойдётся по подписи — так и задумано.
Заказ не мгновенный.
Обычный тестовый заказ идёт около десяти секунд и проходит те же промежуточные статусы, что боевой. Мгновенная выдача научила бы вас коду, который никогда не видел processing.
Сценарии — поле test_scenario при создании заказа
okпо умолчанию: выдача примерно через 10 секунд
fastвыдача на ближайшем тике, 1–5 секунд — для CI
slowвыдача через 3 минуты: проверить, что долгий processing не считается ошибкой
supplier_refusedпровал с кодом SUPPLIER_ERROR, игровые деньги возвращены
invalid_fieldsпровал: «Данные покупателя не прошли проверку»
recipient_unavailableпровал с кодом RECIPIENT_NOT_FOUND
out_of_stockсинхронный 409 в ответе на POST — заказ вообще не создаётся
stuckвисит в работе; через час закрывается по таймауту с возвратом
Зарезервированные получатели
В песочнице /recipient отвечает по таблице и никуда не ходит.
gs_sandbox_okнайден и можно выдать
gs_sandbox_missingне разрешается
gs_sandbox_haspremiumнайден, но Premium уже есть
gs_sandbox_nogiftsнайден, выдать нельзя
gs_sandbox_silentисточник не ответил
gs_sandbox_badдля Steam
Любой другой корректный ник считается найденным.
Управление: GET и POST /sandbox
GET/sandboxбаланс, список сценариев и ников
POST/sandbox {"action":"topup","amount":100}долить игровых денег
POST/sandbox {"action":"advance","order":"ord_test_…"}протолкнуть заказ сейчас
Боевым ключом эти методы отвечают 403 sandbox_only. Чтобы воспроизвести 402 insufficient_funds в CI с первой попытки: выставьте остаток меньше цены и создайте обычный заказ — придёт та же ошибка с теми же полями required и balance, что в бою.
Подробности
Выданные коды каталога выглядят как GS-TEST-XXXX-01 — так их видно на экране, если магазин печатает выдачу покупателю. Звёзды, Premium и Steam выдачи не возвращают ни в песочнице, ни в бою.
Как узнать о выдаче
Опросом. Вебхуков мы не шлём — и это решение, а не задел на будущее.
Заказ создаётся мгновенно, а выдача идёт в фоне — обычно секунды. Чем кончилось, узнаёте сами, и способа два.
Один заказ
GET /orders/{id} — коды приходят там же, как только появятся. Годится, пока заказов немного.
Всё, что изменилось
GET /orders?updated_since=… — что поменялось с этого момента, старые изменения первыми. Это и есть замена вебхуку.
Как следить
// Раз в несколько секунд, начиная с того, что уже видели.
let since = "2026-09-15 08:30:00";
const res = await fetch(
BASE + "/orders?updated_since=" + encodeURIComponent(since) + "&per_page=100",
{ headers: { Authorization: "Bearer gs_live_ваш_ключ" } },
);
const { orders } = await res.json();
for (const order of orders) {
// Обрабатывайте по client_ref — он ваш и переживает что угодно.
handle(order);
// Курсор двигаем по ПОСЛЕДНЕМУ обработанному: ответ отсортирован по// updated_at по возрастанию, поэтому пропустить ничего нельзя.
since = order.updated_at;
}
Подробности
Курсор берите из поля updated_at последнего обработанного заказа, а не из своих часов: наше время и ваше не совпадают, и на расхождении в секунду теряется ровно то изменение, ради которого вы опрашиваете. Сдвигайте его только после обработки — тогда обрыв посреди страницы стоит повтора, а не пропажи.
Подробности
Одно и то же изменение может прийти дважды: курсор по времени, а не по номеру. Делайте обработку идемпотентной по client_ref — тот же приём, что защищает вас от двойной покупки.
Ошибки
Формат один на все методы.
Формат
{ "error": { "code": "unauthorized", "message": "Ключ не найден или проект отключён" } }
У insufficient_funds рядом с message лежат числа — разбирать текст не нужно и не стоит: формулировки мы правим, поля нет. У остальных ошибок числа пока только в тексте; будем добавлять по мере надобности.
401unauthorizedКлюч не передан, неверен, или проект отключён
402insufficient_fundsНе хватает денег на балансе аккаунта
403project_disabledПроект отключён — заказы по нему не принимаются
404not_foundТовара или заказа нет — либо он выключен вами
400invalid_fieldsНе заполнено или слишком длинное поле из fields
409out_of_stockОстаток меньше, чем просите
422not_deliverableЭтот товар через API пока не заказать
422recipient_refusedПолучателю нельзя подарить Premium — причина в message
422term_unavailableЭтот срок Premium сейчас не продаётся
500pricing_errorЦена настроена неверно — напишите нам
503upstream_unavailableЦена у Fragment временно недоступна
503no_ratesКурс для пополнения Steam сейчас неизвестен
429rate_limitedСлишком часто — см. заголовок Retry-After
400bad_requestНекорректный параметр запроса
Деньги на счету
Пополнения по API не будет — это решение, а не очередь. Пополнение живёт в кабинете: USDT в сети TON или TRC20, GRAM, зачисление автоматическое по точной сумме. Планируйте интеграцию из того, что баланс пополняет человек.
Поэтому и о низком балансе мы пишем в бота, а не вебхуком: сообщение приходит туда, где этот человек уже есть — в @gamesend_bot, тот же, через который вы входите. Предупреждаем один раз на подходе к нулю и повторяем только после того, как баланс восстановился. О заказах узнаёте опросом — см. «Как узнать о выдаче».