GameSend

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

API

Подключите наш каталог к своему магазину. Все ответы в JSON, кодировка UTF-8, цены в долларах.

Базовый адрес

Все пути ниже отсчитываются от него.

Ключ выдаётся в личном кабинете, в «Настройках».

Получить доступ

Авторизация

Ключ передаётся в заголовке. Он привязан к вашему аккаунту: цены, баланс и заказы — ваши.

Заголовок
Authorization: Bearer gs_live_ваш_ключ

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

Подробности

Ограничения: 120 запросов в минуту на чтение и 20 в минуту на создание заказов, отдельно для боевого и тестового ключа. При превышении придёт 429 с заголовкомRetry-After— он говорит, через сколько секунд повторить. Заказы ограничены строже намеренно: цикл там тратит деньги, и в отличие от чтения это не отменить ожиданием. У методов Telegram-подарков сверх этого свои, более низкие лимиты на аккаунт — см. «Telegram Gifts».

Работает сейчас

Эти методы доступны и отвечают.

GET/ping

Проверка ключа

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

Запрос
curl https://gamesend.shop/api/v1/ping \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "ok": true,
  "mode": "live",
  "account": { "id": "prj_XXXXXXXX" },
  "project": { "id": "prj_XXXXXXXX", "name": "Мой магазин" }
}

Подробности

mode — live или test. Проверяйте его при старте: ключ, попавший не в тот конфиг, молчит ровно до момента, когда заказ либо не придёт, либо потратит настоящие деньги.

GET/catalog

Каталог

Каталог один на всех, и в нём только то, что есть в наличии. Цены — ваши: сколько за товар платите нам вы.

Параметры

category
Категория: game_keys, gift_cards, topups
search
Поиск по названию
page
Страница, с 1
per_page
Размер страницы, до 200. По умолчанию 50
include
offers — вложить номиналы прямо в список
Запрос
curl "https://gamesend.shop/api/v1/catalog?category=game_keys&per_page=2" \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "items": [
    {
      "id": 1042,
      "name": "Abyssus",
      "category": "game_keys",
      "category_label": "Ключи игр",
      "region": "GLOBAL",
      "image": "https://…/abyssus.jpg",
      "in_stock": 1,
      "cost_from": 17.14,
      "cost_to": 17.14,

      // только с ?include=offers
      "offers": [
        { "id": 5312, "name": "Standard Edition", "cost": 17.14,
          "stock": null, "min_qty": 1, "max_qty": null }
      ]
    }
  ],
  "page": 1,
  "per_page": 2,
  "has_more": true
}

Подробности

cost_* — сколько платите нам вы; розничную цену для своего покупателя ставите у себя. Пагинация идёт по has_more, а не по общему числу: считать точное количество на каждый запрос — лишняя нагрузка, а листать вперёд это не мешает. include=offers вкладывает номиналы прямо сюда — это ровно те же строки и те же цены, что отдаёт /catalog/{id}, читаются одним запросом на всю страницу вместо запроса на каждый товар. Витрине на 50 товаров это экономит 50 обращений. Учтите, что ответ становится заметно больше: если нужен только список названий и вилка цен, не просите.

GET/catalog/{id}

Товар и номиналы

Конкретные суммы пополнения или варианты ключа с ценой каждого. Товар, которого в каталоге нет — снят с продажи или id неверный, — вернёт 404.

Запрос
curl https://gamesend.shop/api/v1/catalog/1187 \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "id": 1187,
  "name": "PUBG Mobile",
  "category": "topups",
  "category_label": "Пополнение игр",
  "region": null,
  "offers": [
    { "id": 88104, "name": "60 UC",  "cost": 0.89,
      "stock": null, "min_qty": null, "max_qty": null },
    { "id": 88105, "name": "325 UC", "cost": 4.41,
      "stock": 42,   "min_qty": 1,    "max_qty": 10 }
  ],
  "fields": [
    { "key": "user_id",   "label": "User ID", "type": "text" },
    { "key": "server_id", "label": "Server",  "type": "select",
      "options": [ { "value": "europe", "label": "Europe" },
                   { "value": "asia",   "label": "Asia"   } ] }
  ]
}

Подробности

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

Баланс

Остаток на счёте аккаунта и последние движения по нему. Тестовым ключом — игровой счёт песочницы.

Запрос
curl https://gamesend.shop/api/v1/balance \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "balance": 24.50,
  "overdraft": 0,
  "available": 24.50,
  "currency": "USD",
  "recent": [
    { "kind": "charge", "amount": -0.82, "balance_after": 24.50,
      "order_id": "ord_XXXXXXXX",
      "comment": "telegram_stars для @ivan", "at": "2026-09-05 12:04:11" },
    { "kind": "topup", "amount": 25.00, "balance_after": 25.32,
      "order_id": null,
      "comment": "Пополнение 25.0000 USDT", "at": "2026-09-05 11:58:02" }
  ]
}

Подробности

available — то, что реально можно потратить прямо сейчас: сравнивайте цену с ним, а не с balance. Обычно это одно и то же число, но если мы открыли вам овердрафт, overdraft показывает разрешённый минус, а balance после его использования становится отрицательным — это не ошибка ответа. Овердрафт по умолчанию нулевой, включается только по договорённости. Исключение — Telegram-подарки: овердрафта у них нет, и цену подарка сравнивайте с balance. order_id — заказ, за который списали: по нему можно найти заказ, ответ на создание которого до вас не дошёл. У пополнений и корректировок он null.

GET/products

Что принимают свои товары

Границы сумм, доступные валюты и обязательные поля — те же самые, по которым заказ проверяется при создании. Нужен, чтобы форма не давала ввести то, что мы потом откажемся принять.

Запрос
curl https://gamesend.shop/api/v1/products \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "products": [
    { "product": "telegram_stars", "required": ["username", "quantity"],
      "quantity": { "min": 50, "max": null, "step": 1, "fractional": false } },

    { "product": "telegram_premium", "required": ["username", "months"],
      "variants": { "months": [3, 6, 12] } },

    { "product": "steam_topup", "available": true,
      "required": ["steam_login", "amount", "currency"],
      "currencies": [
        { "code": "USD", "rate": 1,        "decimals": 2, "min": "0.15",  "max": "1000.00" },
        { "code": "RUB", "rate": 84.2377,  "decimals": 4, "min": "12.6357",
          "max": "84237.7050" }
      ],
      "nominal_usd": { "min": 0.15, "max": 1000 },
      "limit_scope": "order",
      "rates_updated_at": "2026-09-13 14:20:03" },

    { "product": "telegram_gift", "available": true,
      "required": ["gift", "username", "max_price", "client_ref"] }
  ],
  "usd_rub": 87.61
}

Подробности

telegram_gift — Telegram-подарки: цены здесь нет, у каждого лота она своя (GET /gifts/{slug}). available у подарков значит только, что они подключены: этот метод к сервису подарков не ходит и его не ждёт. Открыты ли продажи прямо сейчас, говорит available в карточке лота и в котировке; если закрыты — заказ ответит 503 gifts_unavailable, ничего не списав. Подробно — в разделе «Telegram Gifts». 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

Курс для рублёвой витрины

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

Запрос
curl https://gamesend.shop/api/v1/rates \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "base": "USD",
  "rates": { "RUB": 87.61 },
  "updated_at": "2026-09-16 15:21:19",
  "cache_seconds": 60,
  "note": "Курс для рублёвой витрины: …"
}

Подробности

Это НЕ тот курс, что лежит в /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. Для Telegram-подарка — telegram_gift, см. «Telegram Gifts»
username
Получатель, без @
months
Для Premium: 3, 6 или 12
quantity
Для звёзд: сколько. На то, кто получатель, не влияет
Запрос
curl -X POST https://gamesend.shop/api/v1/recipient \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{ "product": "telegram_premium", "username": "ivan", "months": 3 }'
Ответ
{
  "found": true,
  "can_receive": false,
  "name": "Sergey Drozdov",
  "photo": "https://cdn4.telesco.pe/file/…jpg",
  "reason": "has_premium",
  "resolution": "fragment_ok"
}

Подробности

Три поля, три разных вопроса. 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. Telegram-подарок — telegram_gift, см. «Telegram Gifts»
quantity
Для звёзд и каталога — количество
months
Для Premium: 3, 6 или 12
username
Для Premium: получатель. Без него цена ориентировочная — см. estimate
steam_login
Для Steam не нужен: цена от суммы, а не от аккаунта
amount
Для Steam: сумма зачисления
currency
Для Steam: валюта суммы
Запрос
curl -X POST https://gamesend.shop/api/v1/quote \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "telegram_premium",
    "months": 3,
    "username": "ivan"
  }'
Ответ
{
  "price": 13.19,
  "currency": "USD",
  "kind": "telegram_premium",
  "estimate": false,
  "balance": 24.50,
  "enough": true,
  "recipient": {
    "name": "Sergey Drozdov",
    "photo": "https://cdn4.telesco.pe/file/…jpg"
  }
}

Подробности

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. Telegram-подарок — telegram_gift, см. «Telegram Gifts»
username
Для Telegram: получатель, без @
quantity
Для звёзд — сколько штук, минимум 50; для каталога — сколько единиц
months
Для Premium: 3, 6 или 12
steam_login
Для Steam: логин аккаунта, латиницей
amount
Для Steam: сумма зачисления на аккаунт
currency
Для Steam: валюта суммы, например RUB или KZT
client_ref
Ваш номер заказа, обязателен. Повтор с тем же значением вернёт прежний заказ, а не купит второй
Запрос
curl -X POST https://gamesend.shop/api/v1/orders \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "telegram_stars",
    "username": "ivan",
    "quantity": 50,
    "client_ref": "shop-order-1024"
  }'
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "processing",
    "product": "telegram_stars",
    "username": "ivan",
    "quantity": 50,
    "months": null,
    "price": 0.82,
    "client_ref": "shop-order-1024",
    "created_at": "2026-09-05 12:04:11",
    "delivered_at": null,
    "error": null
  },
  "balance": 24.50
}

Подробности

Обязательно передавайте client_ref. Если запрос отвалится по таймауту, повтор с тем же значением вернёт тот же заказ и не купит второй раз — ответ будет 200 вместо 201.

POST/orders

Пополнение Steam

Тот же метод, но сумму называете вы: amount — сколько зачислить на аккаунт, currency — в какой валюте. Цена считается по курсу на момент заказа.

Запрос
curl -X POST https://gamesend.shop/api/v1/orders \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "steam_topup",
    "steam_login": "ivan_2004",
    "amount": "1000",
    "currency": "RUB",
    "client_ref": "shop-order-1025"
  }'
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "processing",
    "product": "steam_topup",
    "price": 12.34,
    "client_ref": "shop-order-1025",
    "created_at": "2026-09-10 12:04:11",
    "delivered_at": null,
    "error": null
  },
  "balance": 24.50
}

Подробности

Логин, а не отображаемое имя: деньги уходят на тот аккаунт, который назвали, и вернуть их нельзя. 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
Запрос
curl "https://gamesend.shop/api/v1/orders?status=failed&created_from=2026-09-12" \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "orders": [
    {
      "id": "ord_XXXXXXXX",
      "status": "delivered",
      "product": "gift_cards",
      "username": null,
      "quantity": 1,
      "months": null,
      "updated_at": "2026-09-15 08:31:04",
      "price": 0.52,
      "client_ref": "shop-order-1024",
      "created_at": "2026-09-12 12:04:11",
      "delivered_at": "2026-09-12 12:04:47",
      "error": null,
      "item": { "product_id": 395, "product_name": "Roblox (Global)",
                "offer_id": 1025, "offer_name": "800 Robux" }
    }
  ],
  "page": 1,
  "per_page": 50,
  "has_more": true
}

Подробности

Коды выданных товаров в списке НЕ приходят — как и данные покупателя из fields. Нашли нужный заказ, забрали коды через GET /orders/{id}: так один утёкший ключ не выгружает всю историю кодов одним запросом. has_more — это не общее число: при активной торговле страницы съезжают, поэтому для сверки надёжнее сужать created_from и created_to, чем листать вглубь. Заказы Telegram-подарков приходят здесь же, с product: telegram_gift и блоком gift, и фильтр status работает для них так же.

GET/orders/{id}

Статус заказа

Три состояния: processing — в работе, delivered — выдано, failed — не получилось, деньги вернулись на баланс. Здесь же приезжает сам товар: у ключей и подарочных карт коды лежат в delivery.items.

Запрос
curl https://gamesend.shop/api/v1/orders/ord_XXXXXXXX \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "delivered",
    "price": 0.82,
    "delivered_at": "2026-09-05 12:04:47",
    "error": null,

    "item": {
      "product_id": 395,
      "product_name": "Roblox (Global)",
      "offer_id": 1025,
      "offer_name": "800 Robux"
    },
    "fields": null,
    "delivery": {
      "items": [
        { "code": "P2DV-5JALB3-J8BB" }
      ]
    }
  }
}

Подробности

delivery появляется только у товаров каталога и только когда status = delivered: до выдачи там null, а не пустой список. В items столько кодов, сколько прислал поставщик — обычно по одному на единицу quantity, но сверяйте с длиной массива, а не считайте по quantity. У пополнений (topups) кодов нет вовсе, там items пустой: товар ушёл прямо на игровой аккаунт из fields. Для звёзд, Premium и Steam полей item, fields и delivery нет — это не каталог. У Telegram-подарка вместо них блок gift с этапом покупки — см. «Ход покупки» в разделе «Telegram Gifts».

Telegram Gifts

Уникальные подарки Telegram, выставленные на продажу на Fragment: витрина, точная цена, покупка и передача получателю. Работает с 26.09.2026.

Продаётся конкретный лот — один подарок со своим номером, моделью, фоном и узором, выставленный по фиксированной цене. Мы покупаем его и передаём получателю в Telegram: тот становится полноправным владельцем, подарок виден по ссылке t.me/nft/…. В выдаче только лоты «купить сразу» — без аукционов и проданных — и только коллекции из нашего сверенного списка.

Как проходит покупка

  1. 1
    GET/gifts/collections

    Коллекции, которые мы продаём, и значения фильтров к ним — GET /gifts/collections/{collection}. Оба ответа держите у себя: меняются редко.

  2. 2
    GET/gifts

    Лента лотов с ориентировочной ценой; листается по next_offset.

  3. 3
    GET/gifts/{slug}

    Карточка: свойства, картинки, анимация и точная цена для вас.

  4. 4
    POST/recipient

    С product: telegram_gift — примет ли человек подарок. Имя и аватар — покупателю на подтверждение.

  5. 5
    POST/quote

    Точная цена и хватает ли баланса — прямо перед тем, как брать деньги.

  6. 6
    POST/orders

    С max_price — потолком цены из котировки. Деньги списываются сразу.

  7. 7
    GET/orders/{id}

    Или GET /orders?updated_since=… — опрос до delivered или failed. Обычно это минута-две.

  • Цена в ленте — оценка.

    price_estimate считается по цене, которую показывает Fragment, и обычно выше точной, но бывает и чуть ниже. Точную называют карточка и POST /quote — брать деньги с покупателя стоит по ней.

  • Ваше число — только потолок.

    max_price в заказе обязателен. Лот дороже — 409 price_changed с новой ценой, ничего не списано; дешевле — спишется меньшее. Уменьшить счёт ваше число не может.

  • В минус подарки не продаются.

    Овердрафта у подарков нет, даже если он вам открыт: сравнивайте цену с balance, а не с available. Так считает и enough в котировке. Не хватает — 402 insufficient_funds.

  • Один лот — один заказ.

    Пока по лоту идёт заказ — до delivered или failed, в том числе когда подарок уже куплен и ждёт передачи, — второй получит 409 gift_reserved. mine: true и номер в order — это ваш же заказ: следите за ним, а не создавайте новый. mine: false — лот покупает кто-то другой.

  • Лот уникален и может уйти.

    Между карточкой и заказом его могут купить на Fragment. До оплаты это 409 gift_not_buyable, деньги не списаны; после — заказ закончится failed, и деньги вернутся.

  • Возврат — когда подарок точно не куплен.

    Если подарок уже куплен, а передать его не выходит, деньги сами не возвращаются: заказ ждёт на этапе bought, а когда передача остановлена — needs_action говорит почему. Дело в получателе — назовите другого.

  • Лимит на получателя.

    Одному человеку за сутки уходит ограниченное число заказов звёзд, Premium и подарков — считаются все заказы ему, не только ваши. Подарки идут в число заказов, но не в сумму: дорогому подарку лимит по сумме не мешает. Упёрлись — 429 recipient_daily_limit с числами в полях; это не про частоту, и Retry-After у него нет.

  • Доставка — минута-две.

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

  • Картинки и анимация — файлы Fragment.

    images — JPEG 320 и 640 px, animation — Lottie JSON: это не картинка, его проигрывают lottie-web или другим плеером Lottie. Браузер покупателя грузит их прямо с CDN Fragment, ключ не нужен. Навсегда у себя не копируйте: файлы не наши.

  • Каталог не для обхода.

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

Лимиты на аккаунт, в минуту

Сверх общих лимитов ключа — свои у каждого метода подарков, отдельно для боевого и тестового ключа. Чтение сверх лимита — не отказ: ответ приходит из того, что уже прочитано, и 429 rate_limited — только если отдать нечего. У всех таких 429 заголовок Retry-After: 60.

GET/gifts30, дальше — из уже прочитанного
GET/gifts/collections20, дальше — из уже прочитанного
GET/gifts/collections/{collection}20, дальше — из уже прочитанного
GET/gifts/{slug}12, дальше — из уже прочитанного; цены там может не оказаться
POST/quote10, дальше — цена из уже прочитанного
POST/recipient8, дальше — из уже прочитанного; username в /quote считается сюда же
POST/orders6, дальше — 429 сразу
POST/orders/{id}/recipient5, дальше — 429 сразу

Подробности

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

Покупка с потолком
// Точная цена — прямо перед оплатой. Её же отдаёте потолком в заказ.
const quote = await post("/quote", {
  product: "telegram_gift",
  gift: "winterwreath-4614",
  username: "ivan",
});

// …покупатель согласился на quote.price и заплатил вам…

const res = await post("/orders", {
  product: "telegram_gift",
  gift: "winterwreath-4614",
  username: "ivan",
  max_price: quote.price, // дороже не спишется, дешевле — спишется меньшее
  client_ref: "shop-order-1026", // повтор после обрыва вернёт тот же заказ
});

if (res.error?.code === "price_changed") {
  // Лот подорожал, ничего не списано. Новая цена — res.error.price:
  // покажите её покупателю и повторите с новым потолком.
}
GET/gifts/collections

Коллекции

Коллекции подарков, которые мы продаём. Только сверенные нами, поэтому их может быть меньше, чем на Fragment. slug — то, что передаётся в collection у GET /gifts.

Запрос
curl https://gamesend.shop/api/v1/gifts/collections \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "collections": [
    { "slug": "plushpepe", "name": "Plush Pepe", "count": 2847,
      "image": "https://fragment.com/file/gifts/…" },
    { "slug": "winterwreath", "name": "Winter Wreath", "count": 25131,
      "image": "https://fragment.com/file/gifts/…" }
  ],
  "cache_seconds": 600
}

Подробности

count — сколько подарков в коллекции, по данным Fragment. image — картинка коллекции с Fragment, бывает null. Список меняется редко: кэшируйте на десять минут — cache_seconds, и то же в заголовке Cache-Control: private, max-age=600. Лимит — 20 запросов в минуту на аккаунт; сверх него ответ приходит из того, что уже прочитано.

GET/gifts/collections/{collection}

Модели, фоны и узоры

Значения фильтров одной коллекции. У каждого подарка есть модель, фон и узор; value — ровно то, что передаётся обратно в model, backdrop и symbol у GET /gifts.

Запрос
curl https://gamesend.shop/api/v1/gifts/collections/winterwreath \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "collection": "winterwreath",
  "attributes": {
    "model": [
      { "value": "Snowflake", "count": 312,
        "image": "https://fragment.com/file/gifts/…" }
    ],
    "backdrop": [
      { "value": "Onyx Black", "count": 402,
        "image": "https://fragment.com/file/gifts/…" }
    ],
    "symbol": [
      { "value": "Star", "count": 97,
        "image": "https://fragment.com/file/gifts/…" }
    ]
  },
  "cache_seconds": 3600
}

Подробности

count — сколько подарков коллекции с этим значением, от других фильтров не зависит. image — картинка с Fragment, бывает null. Коллекции нет или мы её не продаём — 404 not_found. Эти списки почти не меняются: кэшируйте на час (cache_seconds, Cache-Control: private, max-age=3600). Лимит — 20 запросов в минуту на аккаунт; сверх — из уже прочитанного.

GET/gifts

Лента подарков

Лоты, которые можно купить сразу, по фиксированной цене — без аукционов и проданных, только в коллекциях, которые мы продаём. С ориентировочной ценой, картинками и анимацией.

Параметры

collection
Слаг коллекции из GET /gifts/collections. Без него — все наши коллекции вместе
model
Значение из GET /gifts/collections/{collection}. Повторяется: model=A&model=B. Только вместе с collection, до 100 значений
backdrop
Так же: фон, до 100 значений
symbol
Так же: узор, до 300 значений. Каждое значение — не длиннее 100 символов
search
Поиск, до 64 символов
sort
price_asc — сначала дешёвые (по умолчанию), price_desc — сначала дорогие, listed — недавно выставленные
offset
next_offset прошлой страницы. Первая страница — без него или 0; больше 10000 не принимается
Запрос
curl "https://gamesend.shop/api/v1/gifts?collection=winterwreath&backdrop=Onyx%20Black" \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "items": [
    {
      "slug": "winterwreath-4614",
      "collection": "winterwreath",
      "number": 4614,
      "title": "Winter Wreath #4614",
      "model": "Snowflake",
      "backdrop": "Onyx Black",
      "symbol": "Star",
      "price_estimate": 7.52,
      "images": {
        "preview": "https://nft.fragment.com/gift/winterwreath-4614.medium.jpg",
        "large": "https://nft.fragment.com/gift/winterwreath-4614.large.jpg"
      },
      "animation": "https://nft.fragment.com/gift/winterwreath-4614.lottie.json"
    }
  ],
  "next_offset": 60,
  "has_more": true,
  "currency": "USD"
}

Подробности

price_estimate — ориентир в долларах, а не цена: он считается по цене, которую показывает Fragment, и обычно выше точной. Точную называют GET /gifts/{slug} и POST /quote. Посчитать не из чего — null. Пагинация по offset, а не по page, как у каталога: так листает сам Fragment. Страница — один его шаг, из которого мы убрали всё, что купить нельзя, поэтому она бывает короче и даже пустой при has_more: true. Пустая страница — не конец: листайте по next_offset, пока has_more не станет false, а next_offset — null. sort=ending — 400: эта сортировка про аукционы, а их в ленте нет. Фильтр без collection, слишком много значений или слишком длинное — тоже 400; коллекции нет или мы её не продаём — 404 not_found. Лента у нас в кеше около минуты. Лимит — 30 запросов в минуту на аккаунт; сверх — из уже прочитанного, и 429 rate_limited, только если отдать нечего.

GET/gifts/{slug}

Карточка подарка

Всё о лоте: свойства с редкостью, картинки, анимация, адрес NFT и точная цена для вашего аккаунта — то же, что страница подарка в кабинете.

Запрос
curl https://gamesend.shop/api/v1/gifts/winterwreath-4614 \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "slug": "winterwreath-4614",
  "collection": { "slug": "winterwreath", "name": "Winter Wreath" },
  "number": 4614,
  "title": "Winter Wreath #4614",
  "issued": { "n": 4614, "total": 25131 },
  "attributes": {
    "model": { "value": "Snowflake", "rarity_pct": 1.5 },
    "backdrop": { "value": "Onyx Black", "rarity_pct": 2 },
    "symbol": { "value": "Star", "rarity_pct": 0.8 }
  },
  "images": {
    "preview": "https://nft.fragment.com/gift/winterwreath-4614.medium.jpg",
    "large": "https://nft.fragment.com/gift/winterwreath-4614.large.jpg"
  },
  "animation": "https://nft.fragment.com/gift/winterwreath-4614.lottie.json",
  "links": { "telegram": "https://t.me/nft/WinterWreath-4614" },
  "address": "EQ…",
  "buyable": true,
  "reason": null,
  "price": 6.48,
  "currency": "USD",
  "available": true,
  "reserved": null
}

Подробности

buyable: false — лот сейчас не купить, причина кодом в reason: not_for_sale — продан или снят с продажи; in_telegram — подарок сейчас в Telegram, а не на продаже; collection_not_supported — коллекцию мы не продаём. buyable: null — цену сейчас прочитать не удалось: reason тогда rate_limited (лимит аккаунта, а в кеше цены нет) или gifts_unavailable, и price, available и reserved тоже null. Карточка при этом приходит — рассмотреть подарок можно и без цены. price — точная цена для вас сейчас, у нас она держится около 30 секунд; заказ читает лот заново. available: false — заказы на этот лот сейчас не принимаются: цену показать можно, заказ получит 503 gifts_unavailable. available: null — не знаем: сверх лимита карточка собрана из уже прочитанного, а свежего ответа о продажах по этому лоту у нас нет; заказ проверит сам. reserved: null — лот свободен; {mine: true, order} — его уже покупает ваш заказ; {mine: false} — чужой. Пока reserved не null, новый заказ на лот получит 409 gift_reserved, даже при buyable: true; когда тот заказ подарок уже купил, приходит buyable: false без цены — лот ждёт передачи по нему. rarity_pct — сколько процентов подарков коллекции с тем же значением; issued — номер выпуска и тираж; address — адрес NFT; links.telegram — где подарок видно в Telegram. Чего о подарке сейчас не знаем, то приходит null. Такого подарка нет — 404 not_found. Тестовым ключом цена настоящая, а reserved всегда null и available — true: так лот увидит тестовый заказ. Лимит — 12 запросов в минуту на аккаунт; сверх — из уже прочитанного.

POST/recipient

Примет ли получатель

Тот же метод, что у звёзд и Premium, с product: telegram_gift — и вопрос другой: примет ли человек уникальный подарок. Имя и аватар покажите покупателю до оплаты.

Параметры

product
telegram_gift
username
Получатель, без @
Запрос
curl -X POST https://gamesend.shop/api/v1/recipient \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{ "product": "telegram_gift", "username": "ivan" }'
Ответ
{
  "found": true,
  "can_receive": true,
  "name": "Sergey Drozdov",
  "photo": "https://cdn4.telesco.pe/file/…jpg",
  "reason": "found",
  "resolution": "fragment_ok"
}

Подробности

Поля те же, что у звёзд, — и правило то же: продавайте по can_receive, карточку рисуйте по found, resolution — только в логи. reason: found — примет; not_resolvable — за этим ником никого не удалось получить; unavailable — аккаунт есть, но подарок не примет, например закрыл подарки в настройках приватности; unknown — Fragment не ответил, found и can_receive придут null. Ответ держится у нас минуту, и заказ в эту минуту не ищет получателя заново — поэтому проверяйте перед оплатой, а не на каждое нажатие клавиши. Неверный формат username — 400 bad_request. Лимит — 8 проверок в минуту на аккаунт, в него же идёт username в POST /quote; сверх — ответ только из кеша, а если там пусто — 429 rate_limited с Retry-After: 60.

POST/quote

Цена подарка перед оплатой

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

Параметры

product
telegram_gift
gift
Слаг лота из GET /gifts
username
Необязательно: получатель — тогда он проверяется заодно
Запрос
curl -X POST https://gamesend.shop/api/v1/quote \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "telegram_gift",
    "gift": "winterwreath-4614",
    "username": "ivan"
  }'
Ответ
{
  "product": "telegram_gift",
  "kind": "telegram_gift",
  "gift": "winterwreath-4614",
  "price": 6.48,
  "currency": "USD",
  "estimate": false,
  "buyable": true,
  "reason": null,
  "available": true,
  "reserved": null,
  "balance": 24.50,
  "enough": true,
  "recipient": {
    "name": "Sergey Drozdov",
    "photo": "https://cdn4.telesco.pe/file/…jpg"
  }
}

Подробности

enough — хватает ли balance, без овердрафта: у подарков его нет. Лот, который купить нельзя, — не ответ, а 409 gift_not_buyable с кодом в reason (те же коды, что в карточке), поэтому в ответе 200 buyable — true, а reason — null, кроме одного случая ниже. Лот под заказом и закрытые продажи — не отказ, а поля, как в карточке, и заказ на такой лот не пройдёт. reserved не null — по лоту идёт заказ ({mine: true, order} — ваш, следите за ним), новый получит 409 gift_reserved; пока тот заказ подарок не купил, цена приходит как обычно, а после покупки — buyable: false, reason, price: null и enough: false. available: false — продажи на этот лот закрыты; null — котировка собрана из уже прочитанного (сверх лимита), а свежего ответа о продажах нет. kind — то же значение под тем именем, которым /quote называет товар у звёзд и Premium. С username получатель проверяется так же, как в POST /recipient: не примет — 422 recipient_refused с reason (not_resolvable или unavailable) и recipient внутри error; с боевым ключом name и photo в нём всегда null — Fragment не называет того, кто подарок не примет. Имя и аватар там бывают только в песочнице, у gs_sandbox_nogifts: не стройте на них экран. Fragment не ответил — recipient: null, а цена приходит. Без username поля recipient нет. Слаг не того вида — 400 bad_request, такого подарка нет — 404 not_found, подарки временно недоступны — 503 gifts_unavailable. Лимит — 10 котировок в минуту на аккаунт; сверх — цена из уже прочитанного, пусто — 429 rate_limited с Retry-After: 60. Тестовым ключом: цена настоящего лота, balance — игровой, reserved всегда null, available — true, и в ответе sandbox: true.

POST/orders

Покупка подарка

Создаёт заказ и сразу списывает цену лота — не больше вашего max_price. Дальше мы покупаем подарок и передаём его получателю; ход смотрите опросом.

Параметры

product
telegram_gift
gift
Слаг лота из GET /gifts
username
Получатель, без @
max_price
Потолок цены в USD, обязателен: число или строка с числом. Доли цента отбрасываются: 7.019 — это потолок 7.01. Лот дороже — заказа не будет, дешевле — спишется меньшее
client_ref
Ваш номер заказа, обязателен. Повтор с тем же значением и тем же лотом вернёт прежний заказ, а не купит второй, — даже если первый запрос ещё не ответил
test_scenario
Только тестовым ключом: сценарий песочницы. Боевым — 400
Запрос
curl -X POST https://gamesend.shop/api/v1/orders \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "telegram_gift",
    "gift": "winterwreath-4614",
    "username": "ivan",
    "max_price": 6.48,
    "client_ref": "shop-order-1026"
  }'
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "processing",
    "product": "telegram_gift",
    "username": "ivan",
    "quantity": 1,
    "months": null,
    "price": 6.48,
    "client_ref": "shop-order-1026",
    "created_at": "2026-09-26 12:04:11",
    "updated_at": "2026-09-26 12:04:11",
    "delivered_at": null,
    "error": null,
    "gift": {
      "slug": "winterwreath-4614",
      "title": "Winter Wreath #4614",
      "link": "https://t.me/nft/WinterWreath-4614",
      "recipient_name": "Sergey Drozdov",
      "stage": "paid",
      "needs_action": null,
      "refunded": false
    }
  },
  "balance": 18.02
}

Подробности

Всё проверяется до списания: бронь лота, получатель, лот, ваш потолок и баланс. Лот читается заново, мимо кеша; ответ о получателе — тот же, что у POST /recipient, не старше минуты. 201 — заказ создан и оплачен; 200 — повтор с тем же client_ref и тем же лотом, вернулся прежний заказ и ничего не списано — так же, если повтор пришёл, пока первый запрос ещё шёл. Тот же client_ref на заказе другого вида (звёзды, Premium, Steam, каталог) или на подарке с другим лотом — 409 client_ref_conflict. Отказы, которые стоит обработать: 409 price_changed — лот дороже max_price, новая цена в поле price (покажите её покупателю и повторите с новым потолком); 409 gift_not_buyable — лот купили или сняли, код в reason; 409 gift_reserved — по лоту идёт заказ, и так до его конца, в том числе когда подарок уже куплен и ждёт передачи: mine: true и номер в order — это ваш заказ, следите за ним; mine: false — чужой; 422 recipient_refused — reason not_resolvable или unavailable и recipient внутри error (с боевым ключом name и photo в нём null); 402 insufficient_funds — balance меньше цены, overdraft в нём 0; 429 recipient_daily_limit — суточный лимит на получателя; 503 gifts_unavailable — подарки временно не принимаются, в том числе когда Fragment не ответил о получателе. Заказ ходит к Fragment за лотом, а бывает, и за получателем, и это может занять до минуты: таймаут на своей стороне ставьте не меньше 60 секунд, а после обрыва повторяйте с тем же client_ref. Лимит — 6 заказов-подарков в минуту на аккаунт сверх общего лимита заказов; сверх — 429 rate_limited с Retry-After: 60.

GET/orders/{id}

Ход покупки

Заказ-подарок читается тем же GET /orders/{id} и приходит в GET /orders — с блоком gift вместо item и delivery. status — как у всех: processing, delivered или failed; gift.stage — подробнее, для прогресса на экране покупателя.

Запрос
curl https://gamesend.shop/api/v1/orders/ord_XXXXXXXX \
  -H "Authorization: Bearer gs_live_ваш_ключ"
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "processing",
    "product": "telegram_gift",
    "username": "ivan",
    "price": 6.48,
    "updated_at": "2026-09-26 12:05:02",
    "delivered_at": null,
    "error": null,
    "gift": {
      "slug": "winterwreath-4614",
      "title": "Winter Wreath #4614",
      "link": "https://t.me/nft/WinterWreath-4614",
      "recipient_name": "Sergey Drozdov",
      "stage": "bought",
      "needs_action": {
        "reason": "Получатель не может принять подарок — возможно, закрыл их в настройках приватности или сменил username. Укажите другого получателя.",
        "can_change_recipient": true
      },
      "refunded": false
    }
  }
}

Подробности

gift.stage: paid — оплачен и ждёт очереди; buying — покупаем; bought — куплен, ждёт передачи; sending — передаём получателю; delivered — у получателя, status: delivered; failed — не вышло, status: failed. Всё до delivered и failed — status: processing. needs_action не null, только когда передача остановлена и ждёт решения: reason — текст для человека; can_change_recipient: true — дело в получателе, назовите другого через POST /orders/{id}/recipient; false — задержка на нашей стороне, делать ничего не нужно. Подарок в это время уже куплен, и деньги сами не возвращаются. У failed в error написано, вернулись ли деньги, а gift.refunded: true значит, что возврат уже на балансе; false при failed — он в пути и придёт в течение нескольких минут. link — где подарок видно в Telegram, recipient_name — имя получателя, как его назвал Fragment. quantity у подарка всегда 1.

POST/orders/{id}/recipient

Сменить получателя

Когда передача остановлена из-за получателя — закрыл подарки, сменил username, — назовите другого. Подарок уже куплен и ждёт; новый получатель проверяется так же, как при заказе.

Параметры

username
Новый получатель, без @
Запрос
curl -X POST https://gamesend.shop/api/v1/orders/ord_XXXXXXXX/recipient \
  -H "Authorization: Bearer gs_live_ваш_ключ" \
  -H "Content-Type: application/json" \
  -d '{ "username": "petr" }'
Ответ
{
  "order": {
    "id": "ord_XXXXXXXX",
    "status": "processing",
    "product": "telegram_gift",
    "username": "petr",
    // остальные поля — как в GET /orders/{id}
    "gift": {
      "slug": "winterwreath-4614",
      "title": "Winter Wreath #4614",
      "link": "https://t.me/nft/WinterWreath-4614",
      "recipient_name": "Petr Ivanov",
      "stage": "bought",
      "needs_action": null,
      "refunded": false
    }
  }
}

Подробности

Только пока у заказа gift.needs_action.can_change_recipient: true. В остальное время — 409 bad_state: передача не остановлена из-за получателя (идёт сама или ещё не начиналась — вмешательство только сбило бы её), заказ уже закрыт (delivered или failed) или только что изменился. Перечитайте GET /orders/{id} и решайте по нему. Чужой заказ, не подарок или заказ другого режима — 404 not_found. Новый получатель не примет — 422 recipient_refused с reason; упёрся в суточный лимит — 429 recipient_daily_limit; неверный формат username — 400 bad_request; Fragment не ответил — 503 gifts_unavailable, повторите позже. После смены передача продолжается сама — следите за заказом как обычно. Лимит — 5 вызовов в минуту на аккаунт; сверх — 429 rate_limited с Retry-After: 60. То же самое можно сделать в кабинете, на странице подарка.

Песочница

Второй ключ, gs_test_…, к тому же аккаунту. Тот же каталог, те же цены, те же коды ошибок — но игровые деньги, и ни одного обращения к поставщику или к Fragment, кроме чтения витрины и цен Telegram-подарков: они в песочнице настоящие.

  • Режим решает ключ, а не запрос.

    Поле в теле, которое отменяет списание, — слишком дешёвая вещь, чтобы на ней держалась граница. Какой ключ прислали, такой и режим; подделать нельзя.

  • Миры не пересекаются.

    Тестовый ключ не видит боевых заказов и не может ими управлять, боевой не видит тестовых. Один и тот же 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висит в работе; через час закрывается по таймауту с возвратом
needs_recipientтолько для telegram_gift — см. ниже; у остальных товаров 400

Те же сценарии у Telegram-подарка — product: telegram_gift

okвсе этапы gift.stage — оплачен, покупаем, куплен, отправляем, доставлен — примерно за 10 секунд
fastвсе этапы на ближайшем тике
slowте же этапы за 3 минуты
supplier_refused«лот купили раньше нас»: failed с текстом «Подарок уже купили или сняли с продажи, деньги возвращены», игровые деньги вернулись
out_of_stockсинхронный 409 gift_not_buyable, reason: not_for_sale — заказ не создаётся
recipient_unavailableсинхронный 422 recipient_refused, reason: unavailable
stuckвисит на этапе «покупаем»; через час — failed с возвратом
needs_recipientкуплен и остановлен: needs_action с can_change_recipient: true, пока вы не вызовете POST /orders/{id}/recipient; потом отправляем и доставлен. Час без смены — failed с возвратом
invalid_fields400 bad_request: полей покупателя у подарка нет

Зарезервированные получатели

В песочнице /recipient отвечает по таблице и никуда не ходит.

gs_sandbox_okнайден и можно выдать
gs_sandbox_missingне разрешается
gs_sandbox_haspremiumнайден, но Premium уже есть
gs_sandbox_nogiftsнайден, выдать нельзя
gs_sandbox_silentисточник не ответил
gs_sandbox_badдля Steam

Любой другой корректный ник считается найденным.

У Telegram-подарка те же ники: gs_sandbox_missing и gs_sandbox_nogifts в котировке и заказе дают 422 recipient_refused с reason not_resolvable и unavailable. Два отличия: gs_sandbox_haspremium подарок примет — Premium ему не помеха; gs_sandbox_silent в котировке даёт recipient: null, а в заказе — 503 gifts_unavailable: без ответа о получателе заказ на подарок не принимается и в бою.

Управление: GET и POST /sandbox

GET/sandboxбаланс, список сценариев и ников
POST/sandbox {"action":"topup","amount":100}долить игровых денег
POST/sandbox {"action":"balance","amount":3.00}выставить точный остаток
POST/sandbox {"action":"advance","order":"ord_test_…"}протолкнуть заказ сейчас

Боевым ключом эти методы отвечают 403 sandbox_only. Чтобы воспроизвести 402 insufficient_funds в CI с первой попытки: выставьте остаток меньше цены и создайте обычный заказ — придёт та же ошибка с теми же полями required и balance, что в бою.

Заказ-подарок advance двигает на один этап gift.stage (со сценарием fast — сразу до конца). Остановку needs_recipient он не проходит: это решение за вами, POST /orders/{id}/recipient. В ответе advance — status после толчка, тот же, что в GET /orders/{id}: processing, delivered или failed; у подарка ещё gift.stage. Закрытый заказ — 409 already_closed.

Подробности

Выданные коды каталога выглядят как GS-TEST-XXXX-01 — так их видно на экране, если магазин печатает выдачу покупателю. Звёзды, Premium, Steam и Telegram-подарки выдачи не возвращают ни в песочнице, ни в бою; тестовый подарок проходит те же этапы gift.stage, что боевой.

Как узнать о выдаче

Опросом. Вебхуков мы не шлём — и это решение, а не задел на будущее.

Заказ создаётся мгновенно, а выдача идёт в фоне — обычно секунды, у Telegram-подарков — минута-две. Чем кончилось, узнаёте сами, и способа два.

Один заказ

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 — тот же приём, что защищает вас от двойной покупки.

Подробности

Заказ-подарок меняет updated_at на каждом этапе gift.stage и когда передача останавливается ради нового получателя — курсор это увидит. Остановку с can_change_recipient: true покажите человеку: сама она не пройдёт.

Ошибки

Формат один на все методы.

Формат
{ "error": { "code": "unauthorized", "message": "Ключ не найден или проект отключён" } }

У insufficient_funds рядом с message лежат числа — разбирать текст не нужно и не стоит: формулировки мы правим, поля нет. Так же устроены recipient_daily_limit (recipient, orders_today, spent_today, max_orders, max_amount, retry_after_hours), recipient_refused (recipient с name и photo, у подарка ещё reason, а name и photo у боевого подарка — null) и ошибки Telegram-подарков — их поля описаны в том разделе. У остальных ошибок числа пока только в тексте; будем добавлять по мере надобности.

insufficient_funds
{
  "error": {
    "code": "insufficient_funds",
    "message": "Не хватает средств: нужно $12.24, на балансе $0.69",
    "required": 12.24,
    "balance": 0.69,
    "available": 0.69,
    "overdraft": 0,
    "currency": "USD"
  }
}

Коды

401unauthorizedКлюч не передан или неверен (например, перевыпущен), либо доступ к API закрыт
402insufficient_fundsНе хватает денег на балансе аккаунта
403project_disabledДоступ к API закрыт — заказы не принимаются
403sandbox_onlyМетод песочницы вызван боевым ключом
409already_closedПесочница: advance для заказа, который уже закрыт
404not_foundТовара, подарка или заказа нет — или заказ не ваш
400invalid_fieldsНе заполнено или слишком длинное поле из fields
409out_of_stockОстаток меньше, чем просите
409client_ref_conflictЭтот client_ref уже стоит на заказе другого вида товара или на подарке с другим лотом. Остальное тело не сравнивается: повтор с другим номиналом, количеством, сроком или суммой вернёт прежний заказ
409gift_not_buyableЛот сейчас не купить — код причины в reason
409gift_reservedПо лоту идёт заказ, до его конца — в mine и order, чей
409price_changedЛот дороже вашего max_price — новая цена в price
409bad_stateСменить получателя сейчас нельзя: передача не ждёт нового получателя, заказ закрыт или только что изменился
422not_deliverableЭтот товар через API пока не заказать
422recipient_refusedПолучатель не примет выбранное — причина в message, у подарка ещё и в reason
422term_unavailableЭтот срок Premium сейчас не продаётся
500pricing_errorЦена настроена неверно — напишите нам
500internalСбой на нашей стороне — повторите с тем же client_ref
503upstream_unavailableЦена у Fragment временно недоступна
503no_ratesКурс для пополнения Steam сейчас неизвестен
503gifts_unavailableПодарки временно не продаются — повторите позже
429rate_limitedСлишком часто — см. заголовок Retry-After
429recipient_daily_limitСуточный лимит на одного получателя — числа в полях ошибки
400bad_requestНекорректный параметр запроса

Деньги на счету

Пополнения по API не будет — это решение, а не очередь. Пополнение живёт в кабинете: USDT в сети TON или TRC20, GRAM, зачисление автоматическое по точной сумме. Планируйте интеграцию из того, что баланс пополняет человек.

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

Текущий остаток машинно — GET /balance.