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

Платежи в Telegram-боте: сценарий, статусы и безопасная проверка
Платежи в Telegram-боте: сценарий, статусы и безопасная проверка

Для спроектированного платёжного сценария необходимо создать счёт с уникальным идентификатором, получить подтверждение от провайдера через Bot API, проверить статус на стороне сервера без доверия клиентскому сообщению, обеспечить идемпотентность обработки повторных уведомлений и сохранить возможность возврата средств. Клиентский статус используется только для интерфейса, фактическая выдача товара или услуги происходит после серверной проверки платежа.

Общая архитектура платёжного сценария

Для цифровой подписки внутри Telegram действует отдельное правило: оплата идёт исключительно в Telegram Stars. Бот формирует счёт через sendInvoice с валютой XTR, Telegram показывает счёт и после оплаты передаёт боту successful_payment. Внешний платёжный провайдер в этом сценарии не участвует. Физические товары и офлайн-услуги имеют другой режим оплаты и здесь не рассматриваются.

Основная сложность заключается в том, что Bot API предоставляет HTTP-интерфейс для отправки сообщений и обработки обновлений, но не гарантирует однократную доставку событий. Это означает, что уведомление о платеже может прийти дважды, может быть задержано или может быть подделано злоумышленником, если webhook не защищён.

Архитектура должна учитывать асинхронность: между отправкой счёта и получением подтверждения проходит время, в течение которого пользователь может отменить платёж, закрыть форму или столкнуться с ошибкой провайдера. Бот должен корректно обрабатывать все эти состояния без блокировки других запросов.

Создание счёта и параметры invoice

Метод sendInvoice получает chat_id, title, description, payload, currency и prices. Для цифровых товаров и услуг в Stars укажите currency: "XTR"; в примере Bot API для sendInvoice передаётся пустая строка provider_token: "". Для физических товаров действуют другие правила платежей. payload — строка до 128 байт, которая возвращается в pre_checkout_query и successful_payment и связывает счёт с заказом в базе.

Для XTR массив prices должен содержать ровно один элемент LabeledPrice: например {"label":"30 дней доступа","amount":500}. Здесь amount — целое число Stars, а не копейки. Если нужно автопродление, добавьте допустимый Bot API параметр subscription_period; период и лимит цены нужно брать из актуальной документации, а не зашивать в бизнес-логику.

При формировании payload рекомендуется включать идентификатор заказа из вашей системы, временную метку и контрольную сумму. Это позволяет однозначно связать уведомление с записью в базе и проверить, что payload не был изменён. Например: order_12345_1696348800_hash. Хранение такой структуры в базе до получения подтверждения упрощает отладку и аудит.

Процесс подтверждения платежа

После успешной оплаты Telegram отправляет боту update типа pre_checkout_query, а затем message с полем successful_payment. Pre-checkout приходит до списания средств и позволяет боту отклонить платёж методом answerPreCheckoutQuery, если товар закончился или заказ стал невалидным. Если бот не ответит в течение 10 секунд, платёж отменяется автоматически.

В pre_checkout_query содержатся поля: id (идентификатор запроса), from (данные пользователя), currency, total_amount, invoice_payload. Последнее поле содержит тот же payload, который был передан при создании счёта. На этом этапе нужно проверить, существует ли заказ, доступен ли товар, не истёк ли срок действия счёта. Если всё в порядке, вызывается answerPreCheckoutQuery с параметром ok=true.

После успешной оплаты бот получает message.successful_payment с currency, total_amount, invoice_payload, telegram_payment_charge_id и provider_payment_charge_id. В сценарии Stars опорным ключом для идемпотентности и возврата служит telegram_payment_charge_id. Оба идентификатора можно хранить для аудита, но нельзя подменять один другим в refundStarPayment.

Статус Тип update Действие бота Результат
Счёт создан — sendInvoice Пользователь видит кнопку «Оплатить»
Проверка перед оплатой pre_checkout_query answerPreCheckoutQuery Платёж разрешён или отклонён
Платёж успешен message.successful_payment Выдача товара/услуги Заказ выполнен, запись в БД
Возврат — refundStarPayment Средства возвращены (для Telegram Stars)
Идемпотентность обработки уведомлений
Идемпотентность обработки уведомлений

Идемпотентность обработки уведомлений

Telegram не гарантирует однократную доставку обновлений через webhook или getUpdates. Уведомление о successful_payment может прийти дважды из-за повторной отправки, сбоя сети или перезапуска сервера. Если обработчик не идемпотентен, товар может быть выдан дважды, баланс начислен повторно, а это финансовая потеря.

Идемпотентность Stars строится на уникальном telegram_payment_charge_id. Перед выдачей доступа сервер атомарно вставляет этот ключ в таблицу платежей. Повторная доставка того же Update с тем же update_id и тем же charge ID не должна второй раз выдавать доступ.

Пример схемы: processed_payments (telegram_charge_id TEXT PRIMARY KEY, user_id BIGINT, order_id TEXT UNIQUE, amount_stars INTEGER, processed_at TIMESTAMP). Команда INSERT ... ON CONFLICT DO NOTHING RETURNING telegram_charge_id даёт обработчику однозначный ответ: строка вставлена — можно продолжать; конфликт — платёж уже принят.

Важно выполнять проверку и вставку в одной транзакции базы данных. Если между SELECT и INSERT произойдёт параллельный запрос, оба обработчика могут не найти запись и выдать товар дважды. Использование транзакции с уровнем изоляции SERIALIZABLE или атомарного INSERT ON CONFLICT DO NOTHING предотвращает эту гонку.

Безопасная проверка статуса без доверия клиенту

Клиентское приложение может быть модифицировано: злоумышленник способен отправить webhook с поддельным successful_payment, изменить payload или подменить chat_id. Доверять данным, пришедшим от клиента, нельзя. Все критические проверки выполняются на сервере.

Первый уровень защиты — проверка secret_token в заголовке X-Telegram-Bot-Api-Secret-Token, который задаётся при установке webhook методом setWebhook. Bot API отправляет этот токен с каждым запросом. Если заголовок отсутствует или не совпадает с сохранённым значением, запрос отклоняется с кодом 403. Это предотвращает обращения с неизвестных адресов.

Второй уровень — проверка подписи update. Telegram не предоставляет криптографическую подпись обновлений, но проверка secret_token и HTTPS-соединения обеспечивают аутентичность источника. Дополнительно можно проверить, что IP-адрес запроса принадлежит диапазону серверов Telegram (информация публикуется в FAQ).

Третий уровень — валидация payload. Если payload содержит контрольную сумму (HMAC или хэш от идентификатора заказа и секретного ключа), бот пересчитывает её и сравнивает с полученной. Несовпадение означает попытку подделки. Также проверяется, что заказ с указанным order_id существует, принадлежит пользователю из update.message.from.id и находится в статусе «ожидает оплаты».

Клиентский статус (например, сообщение «Оплата прошла успешно») используется только для улучшения интерфейса. Фактическая выдача товара происходит только после получения successful_payment на сервере, проверки идемпотентности и валидации всех полей. Это исключает сценарий, при котором пользователь видит успех, но товар не получает, или наоборот.

Обработка возвратов и отмен

Полный возврат Stars выполняется методом refundStarPayment. Метод принимает user_id и именно telegram_payment_charge_id. Перед вызовом проверьте в своей базе, что charge ID принадлежит этому пользователю и заказ ещё не был возвращён.

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

В сигнатуре refundStarPayment нет параметра суммы, поэтому нельзя описывать частичный возврат Stars как поддерживаемую операцию. Если нужна другая компенсация, её правила и учёт нужно проектировать отдельно и не выдавать за частичный возврат исходного платежа.

После успешного возврата необходимо обновить статус заказа в базе данных, отменить выданные права доступа (если это подписка) и уведомить пользователя. Уведомление отправляется методом sendMessage с кратким описанием возврата и ссылкой на поддержку, если требуется.

Условный пример: подписка на закрытый канал
Условный пример: подписка на закрытый канал

Условный пример: подписка на закрытый канал

Представим бота, который продаёт цифровой доступ к закрытому каналу за 500 Stars. Бот создаёт заказ pending и вызывает sendInvoice: currency: "XTR", prices: [{"label":"30 дней доступа","amount":500}], а provider_token отсутствует. Для разового 30-дневного доступа subscription_period не нужен; для автопродления его указывают по актуальным правилам Bot API.

Пользователь нажимает «Оплатить» и подтверждает списание Stars; данные карты бот не получает. Telegram отправляет pre_checkout_query. Бот сверяет заказ, пользователя, XTR, сумму и payload, затем отвечает через answerPreCheckoutQuery.

После successful_payment бот атомарно сохраняет telegram_payment_charge_id. Только обработчик, который вставил новую строку, выдаёт доступ и меняет статус заказа. Повторная доставка с тем же charge ID завершается без второй выдачи.

Через неделю пользователь запрашивает возврат из-за неактуальности контента. Администратор проверяет запрос, вызывает refundStarPayment (или API провайдера), обновляет статус заказа на «refunded», удаляет пользователя из канала и отправляет уведомление. Если уведомление о successful_payment придёт повторно, проверка processed_payments предотвратит повторное добавление в канал.

Типичные ошибки при реализации платёжного сценария

Отсутствие проверки идемпотентности. Обработчик выдаёт доступ при каждом получении successful_payment. Повторная доставка тогда приведёт к дублю. Решение для Stars: атомарно фиксировать telegram_payment_charge_id и продолжать только при новой вставке.

Доверие клиентскому статусу. Бот проверяет только наличие сообщения successful_payment от пользователя, не проверяя источник. Злоумышленник отправляет поддельное сообщение через модифицированный клиент и получает товар бесплатно. Решение: обрабатывать только обновления, пришедшие через webhook с корректным secret_token.

Отсутствие таймаута для pre_checkout. Бот выполняет долгий запрос к внешнему API для проверки доступности товара. Если запрос занимает более 10 секунд, Telegram отменяет платёж, но бот продолжает обработку и резервирует товар. Решение: устанавливать таймаут для внешних запросов не более 5–7 секунд, возвращать ошибку, если проверка не завершена.

Игнорирование валидации payload. Злоумышленник перехватывает payload от другого заказа и подставляет его в свой платёж, получая чужой товар. Решение: включать в payload контрольную сумму от order_id и user_id, проверять совпадение user_id из payload и update.message.from.id.

Отсутствие логирования возвратов. Администратор выполняет возврат, но не сохраняет причину и дату. При повторном запросе от того же пользователя невозможно определить, был ли возврат уже выполнен. Решение: создавать запись в таблице refunds с полями order_id, refund_reason, refunded_at, admin_id.

Чек-лист проектирования платёжного сценария

  • Создать таблицу orders с полями: order_id, user_id, status, payload, amount, created_at, updated_at.
  • Создать таблицу processed_payments с уникальным индексом по telegram_payment_charge_id.
  • Установить webhook методом setWebhook с параметром secret_token, сохранить токен в конфигурации.
  • Реализовать проверку заголовка X-Telegram-Bot-Api-Secret-Token в обработчике webhook, возвращать 403 при несовпадении.
  • В методе sendInvoice формировать payload, содержащий order_id, timestamp и контрольную сумму от order_id + user_id + секретный ключ.
  • В обработчике pre_checkout_query проверить существование заказа, доступность товара, валидность payload, вызвать answerPreCheckoutQuery в течение 5–7 секунд.
  • В обработчике successful_payment атомарно вставить telegram_payment_charge_id и выдать доступ только при успешной вставке.
  • Обернуть проверку и вставку в транзакцию базы данных с уровнем изоляции, предотвращающим одновременную обработку.
  • Реализовать метод возврата: проверить статус заказа (должен быть «completed»), вызвать API провайдера, обновить статус на «refunded», отменить доступ.
  • Логировать все этапы: создание счёта, pre_checkout, successful_payment, возвраты — с указанием order_id, user_id, timestamp для аудита.
Безопасность и защита от мошенничества
Безопасность и защита от мошенничества

Безопасность и защита от мошенничества

Помимо проверки secret_token, рекомендуется ограничить доступ к webhook по IP-адресам. Официальный FAQ описывает, что запросы приходят с серверов Telegram, диапазоны IP можно получить из системного API или документации. Настройка firewall или reverse proxy для приёма запросов только с этих адресов снижает риск атак.

Защита от повторных платежей с одним payload: после успешной оплаты изменить статус заказа на «completed» и отклонять pre_checkout для этого заказа при последующих попытках. Это предотвращает ситуацию, когда пользователь случайно или намеренно оплачивает один счёт дважды.

Мониторинг аномалий: если за короткое время приходит множество successful_payment от одного user_id или с одинаковым payload, это может указывать на попытку эксплуатации уязвимости. Автоматическая блокировка таких запросов и уведомление администратора снижает ущерб.

Секретный ключ для HMAC и токен бота должны находиться в переменных окружения или защищённом хранилище, а не в коде. Для Stars provider_token не нужен. Бот не получает номер карты; в логи не записывают токены, секреты или полный платёжный payload.

Часто задаваемые вопросы

Можно ли использовать update_id для проверки идемпотентности?

update_id можно использовать для дедупликации доставки Update: при повторной доставке Telegram повторяет тот же Update, а не создаёт новый update_id. Для платёжной бизнес-операции нужен и более сильный ключ — telegram_payment_charge_id: он защищает выдачу даже при ошибке в транспортном слое.

Что делать, если pre_checkout_query не пришёл?

Если пользователь закрыл форму оплаты до ввода данных карты, pre_checkout_query не генерируется. Заказ остаётся в статусе «pending». Рекомендуется устанавливать срок действия счёта (например, 24 часа) и автоматически переводить неоплаченные заказы в статус «expired» фоновой задачей.

Как обрабатывать частичную оплату или промокоды?

Telegram не поддерживает ввод промокода в форме оплаты. Промокод обрабатывается до создания счёта: пользователь отправляет команду с кодом, бот проверяет валидность, применяет скидку, формирует prices с учётом новой суммы. Частичная оплата требует создания нескольких счётов или реализации логики рассрочки на стороне бота.

Нужно ли хранить telegram_payment_charge_id?

Да. Этот идентификатор связывает платёж с записью в Telegram и требуется для возврата через Telegram Stars. Для возвратов через внешних провайдеров используется provider_payment_charge_id, но хранение обоих упрощает отладку и поддержку.

Как проверить, что провайдер успешно обработал возврат?

Метод refundStarPayment возвращает булево значение: true при успешном возврате, false при ошибке. Для внешних провайдеров проверяется HTTP-код ответа API и статус транзакции. Рекомендуется также сохранять refund_id, возвращённый провайдером, для последующей сверки.

Вывод

Для цифровой подписки в Telegram используйте Stars: XTR, один элемент prices и без provider_token. Перед выдачей доступа сверяйте заказ в pre_checkout_query, а после successful_payment атомарно фиксируйте telegram_payment_charge_id. Этот же идентификатор передавайте в refundStarPayment. Дедупликация по update_id защищает транспортный слой, а уникальный charge ID — саму платёжную операцию. Транзакции и ограничения в базе снижают риск дублей, но не заменяют мониторинг и разбор незавершённых операций.