Перейти к содержанию
RSC
8 сентября 2026 г. · 7 мин чтения

Автовыдача в Telegram-боте: схема заказа, которая не спишет дважды

У Telegram-бота после оплаты одна задача: создать заказ, получить код и отдать его в чат. Сложное здесь — деньги: повторный запрос после таймаута списывает второй раз, поллинг без бэкоффа выжигает лимит, а автовозврат кладёт сумму на ваш баланс, пока покупатель ждёт в чате. Ниже — схема с настоящими статусами и строками ошибок RSC API v1.

Что автоматизируется и где ломается

Три шага: покупатель платит боту, бот создаёт заказ через REST, бот отдаёт в чат код или подтверждение. Деньги двигаются на двух шагах, и это разные деньги: покупатель платит вам, а заказ оплачивается с вашего баланса на площадке — пополненного заранее в USDT (TRC-20, BEP-20, TON, Aptos).

  • Запрос отвалился по таймауту, и бот отправил его снова. Идемпотентности нет, поэтому повтор — это второй платный заказ: списано дважды, один код никому не продан.
  • Бот опрашивает статус раз в секунду прямо из обработчика чата. Несколько покупателей разом — и минутный лимит кончился, а 429 прилетает тому запросу, который забрал бы код.
  • Заказ выполнить нельзя, площадка возвращает деньги — вам, а не покупателю. В боте ничего не изменилось, а минута молчания превращается в отзыв.

Четыре состояния и что покупатель видит в каждом

Всё держится на записи в вашей базе, созданной до обращения к API и живущей дольше него. Сообщение покупателю следует из состояния, а не из последнего HTTP-ответа.

  • new — покупатель заплатил вам. В записи ваш идентификатор заказа, товар и ваша цена. Бот: создать заказ.
  • placing — запрос отправлен или неизвестно, дошёл ли. Бот: ждать и сверяться, повторять запрос отсюда нельзя.
  • waiting — есть номер заказа на площадке, статус created или processing. Бот: опрашивать по расписанию. Покупатель: «заказ №… в работе, код придёт в этот чат».
  • done / refunded — конечные. Либо коды сохранены и отправлены, либо списание вернулось на ваш баланс и вы должны вернуть деньги покупателю.

Совет

Правило, которое спасает от двойного списания: запись в состоянии placing создаётся ДО отправки запроса, и сдвинуть её вперёд может только номер заказа с площадки — но не ошибка HTTP.

Шаг 1. Ключ и первый запрос

Ключ выдаётся вместе с аккаунтом и лежит в разделе «API» кабинета: rsc_live_ и 48 hex-символов, передаётся заголовком Authorization: Bearer <ключ>. Он тратит ваш баланс, поэтому живёт только в переменных окружения сервера. Утёк — перевыпустите на странице профиля, старый умирает сразу.

GET /api/v1/me — проверка живости: статус аккаунта, balance_usd, total_orders. Без валидного ключа придёт 401 и тело {"error":{"type":"unauthorized","message":"…"}} — конверт у всех ошибок одинаковый.

Шаг 2. Каталог и цены

Цены каталога уже ваши: price_usd — это то, что спишется с баланса, умноженное на количество. Розницу считайте от этого числа, а не от номинала. Суммы приходят строками с четырьмя знаками после точки ("2.8100") — разбирайте как десятичные, не как float.

Каталог кешируйте на минуты, а не на секунды, и сбрасывайте кеш, когда заказ вернулся с unavailable или out_of_stock. У позиций гифт-карт есть min_quantity и max_quantity — от 1 до 100 кодов в заказе; пополнение игры всегда одно на заказ.

Шаг 3. Создание заказа

На каждое направление свой эндпоинт: POST /api/v1/gift-cards/order (category_id, card_id, quantity), POST /api/v1/top-ups/order (category_id, offer_id и fields, ключи которого совпадают с fields[].key категории), POST /api/v1/steam-topup/order (steam_login, currency, amount). Баланс списывается в момент создания, в ответе — number, status, charged_usd и channel. Сохраняйте number той же записью, которой переводите строку в waiting.

HTTP 200 не означает «выдано» и даже не означает «в работе»: если заказ не удалось разместить, списание возвращается тут же, и первый же ответ может прийти со статусом refund. Читайте status из тела ответа.

После таймаута не отправляйте запрос повторно. Запросите GET /api/v1/orders?limit=20 — новые сверху — и найдите заказ, созданный примерно в момент вашего запроса: нужный тип, тот же card_id или offer_id и fields, channel api. Нашёлся — забирайте его number. Не нашёлся, а баланс не изменился — заказывайте заново.

Шаг 4. Поллинг статуса

GET /api/v1/orders/{number} отдаёт заказ вместе с полной status_history. Опрашивайте, пока статус created или processing; конечные — completed, failed и refund, строчными буквами, ровно в таком написании.

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

Шаг 5. Выдача и товары, у которых кодов нет

Коды гифт-карт приходят в массиве codes, ключи игр — в keys, и оба заполняются только когда статус стал completed; до этого массив пустой. Пустой массив не ошибка — он означает «ещё нет».

У пополнений игр кодов нет вообще: валюта уходит на аккаунт игрока по указанному идентификатору, пополнение Steam — на логин, а завершение заказа и есть выдача. Значит, текст в чате отличается: на заказ с кодом бот отправляет код и инструкцию по активации, на пополнение — «зачислено на ID …».

Совет

Сохраняйте коды в свою базу до отправки сообщения в Telegram, а состояние done ставьте только после успешной отправки.

Деньги, когда что-то пошло не так

Если заказ выполнить нельзя — позиция закончилась, аккаунт или регион не приняли, истекло время ожидания, — площадка сама возвращает полную сумму на ваш баланс. Статус становится refund, в истории баланса появляется строка с номером заказа, деньги доступны сразу. Без тикета.

Покупателю бота при этом не возвращается ничего: он платил вам тем способом, о котором площадка не знает. Поэтому в ветке refunded два действия: вернуть деньги покупателю и написать ему в чат с номером заказа.

failed и refund — разные вещи: failed означает, что выдать не удалось, refund — что списание вернулось, и целым ваш баланс делает только refund. Заказ, застрявший в failed и не ставший возвратом, — повод написать тикет в кабинете или в Telegram @supportresellcodes: почты у проекта нет.

Разбор ошибок по строкам

Любая ошибка — это {"error":{"type":"…","message":"…"}} и соответствующий код: 400 invalid_request, 401 unauthorized, 403 forbidden, 404 not_found, 429 rate_limited. Ветвитесь по type и по вызванному эндпоинту, но не по тексту message.

  • insufficient_balance — 400. Заказ не создан, ничего не списано. Повторять бессмысленно: верните деньги покупателю или поставьте его в очередь и пополните баланс. Проверка порога balance_usd превращает аварию в предупреждение.
  • provider_busy — 429 с сообщением про занятую мощность и БЕЗ заголовка Retry-After. Это не ваш лимит: окно выдачи по позиции переполнено, списания не было. Повторите один раз через минуту, потом верните деньги.
  • missing_field_<ключ> — 400, «Field player_id is required». Бот не собрал поле, которое объявляет категория, — это баг формы, а не временный сбой.
  • invalid_field_<ключ> — 400. Значение пришло, но не годится: пустое после обрезки пробелов или длиннее 200 символов. Попросите покупателя ввести это поле заново.
  • 401 unauthorized — ключа нет, он битый или перевыпущен; 403 forbidden — аккаунт заблокирован или адрес в блоке. Останавливайте воркер и пишите тикет.
  • 429 rate_limited с заголовком Retry-After — вот это ваш лимит. Спите ровно столько секунд, сколько там написано.
  • server_error (category_unavailable, not_configured, network) — чтение повторить можно, вслепую повторять создание заказа, который мог уже списаться, нельзя.

Лимиты и как их не выжечь

Остаток лимита приходит в заголовках каждого ответа: X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute и такая же пара на сутки. Новый ключ стартует с 30 запросов в минуту и 5 000 в сутки; лимиты индивидуальные и по запросу поднимаются, поэтому читайте заголовки, а не зашивайте числа в код.

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

  • Один процесс, одна очередь: воркер идёт по записям в состоянии waiting по времени следующего опроса, а не цикл в каждом обработчике чата.
  • Держите запас: как только X-RateLimit-Remaining-Minute падает ниже пяти, ставьте очередь на паузу до следующей минуты. Создание заказа не должно упираться в лимит.
  • Не ищите работу опросом списка заказов — что открыто, знает ваша база.

Чего у нас нет и как с этим жить

  • Вебхуков нет. О выдаче вы узнаёте поллингом GET /orders/{number}, регистрировать callback-адрес негде.
  • Идемпотентности при создании заказа нет. Замена — ваш собственный идентификатор заказа, записанный до запроса, плюс сверка списком вместо слепого повтора.
  • Песочницы и тестового ключа нет: первый прогон идёт по живому API и с живого баланса. Это сделано дешёвым специально — минимальное пополнение $3, минимального заказа нет, а входной пакет пополнения (они стоят центы) или перевод $0.15 на кошелёк Steam проводит через весь цикл.

Заказы из бота и заказы, оформленные руками в кабинете, живут в одном аккаунте, на одном балансе и в одной истории; у каждого заказа есть поле channel со значением api или panel.

Чек-лист перед запуском бота в бой

  • Запись о заказе с вашим идентификатором создаётся до отправки запроса.
  • status читается из тела ответа на создание, случай refund в нём обработан.
  • Таймауты разбираются сверкой через GET /orders; ни одна ветка кода не повторяет создание заказа.
  • Один поллер, одна очередь, бэкофф по возрасту, потолок в чате и медленная дорожка после него.
  • Пустой массив codes трактуется как «ещё нет»; в сообщениях про пополнения слова «код» нет.
  • Ветка refund сама возвращает деньги покупателю и сама пишет ему в чат.
  • Первый живой заказ сделан на дешёвой позиции и просмотрен целиком в кабинете.

Статусы, типы ошибок, заголовки и лимиты выше — это API в том виде, в каком он описан на день публикации: руководство лежит на /docs, справочник — на /docs/reference, оба вне языковых префиксов (именно /docs, а не /en/docs).

Соберите интеграцию на этой неделе

Создайте аккаунт, заберите API-ключ в кабинете и прогоните весь цикл на балансе в $3 — до того, как бота увидит первый покупатель.