Безопасность платежей в Telegram-боте держится на одном простом принципе: серверу нельзя верить тому, что прислал клиент. Если бот засчитывает оплату по сообщению из фронтенда, кнопке или локальному стейту приложения, у него можно увести баланс за пя…
400 000+ органических переходов за 3 месяца. Со-основатель GoBanana (231K пользователей, 12+ млн ₽ без рекламы) и NeuroScribe (65K пользователей). SEO/GEO-стратегии для AI-поисковиков, 1 700+ единиц контента, 17+ реализованных стратегий.
Об авторе →Claude Code: новый CLI-агент от Anthropic
Anthropic выпустила Claude Code — терминальный AI-агент для разработчиков. Инструмент работает прямо в командной строке и умеет писать, редактировать и запускать код.
Zcode AI: Полный гид по визуальному интерфейсу для Claude Code и AI-агентов
Узнайте, как использовать Zcode для управления Claude Code, Gemini и Codex в едином GUI. Настройка провайдеров, MCP-серверов и визуальный вайбкодинг.
YouTube-канал с монетизацией из любой точки мира: Пошаговый гайд 2026
Инструкция по созданию YouTube-канала: обход блокировок SMS, настройка расширенных функций через виртуальные номера и правила безопасности для монетизации.
Windsurf Code Maps: Как глубоко понимать архитектуру проекта перед написанием кода
Полный гайд по Windsurf Code Maps, модели Sway 1.5 и Sway Grep. Узнайте, как визуализировать архитектуру кода и ускорить разработку в 13 раз.
Vk Fast Cash Strategy
Аудитория ВКонтакте — это те же люди, что и в Instagram, но 'социальный контракт' площадки другой. Если Instagram — это 'дорогой ресторан' с демонстрацией успеха, то VK — это 'душевная шашлычная'. Здесь не работает глянцевый 'успешный успех
Безопасность платежей в Telegram-боте держится на одном простом принципе: серверу нельзя верить тому, что прислал клиент. Если бот засчитывает оплату по сообщению из фронтенда, кнопке или локальному стейту приложения, у него можно увести баланс за пять минут без единого рубля на счете ЮKassa. Ниже разберем пошагово, как проверять webhook ЮKassa и событие successful_payment на сервере, а не доверять глазам пользователя.
Главный вывод: доверять можно только серверным событиям от Telegram и ЮKassa, проверенным по IP и статусу объекта. ЮKassa присылает уведомления с фиксированного списка IP-адресов, а Telegram шлет successful_payment только после реального списания. Ниже показан код для aiogram, таблица проверок и разбор частых дыр в готовых гайдах на YouTube.

Клиентское приложение или Mini App может отправить боту любое сообщение, включая поддельное "оплата прошла". Единственный источник правды это сервер платежной системы или сервер Telegram.
Пользователь управляет своим устройством полностью. Он может перехватить запрос в DevTools, подменить callback_data кнопки или просто написать боту нужный текст руками. Если хендлер засчитывает баланс по фразе вроде "оплатил" или по факту нажатия кнопки "Я оплатил", защиты там ноль.
Разработчики часто натыкаются на это уже на проде, когда цифры в базе не бьются с выпиской ЮKassa. Причина почти всегда одна: где-то в коде баланс обновляется до подтверждения от платежной системы, а не после. Разберем, откуда берется настоящее подтверждение и как его проверить.

Telegram присылает два события: pre_checkout_query перед списанием и successful_payment после него. Только второе означает, что деньги реально ушли.
Официальная документация Bot Payments API прямо предупреждает: нужно всегда дожидаться <cite index="21-1">события successful_payment перед выдачей товара, потому что сам по себе ответ на pre_checkout_query еще не гарантирует успешный заказ или платеж</cite>. Это ключевая деталь, которую пропускают в половине туториалов на YouTube.
Когда пользователь жмет кнопку оплаты в инвойсе, Telegram шлет боту pre_checkout_query с полными данными о заказе. Бот обязан ответить методом answerPreCheckoutQuery в течение 10 секунд, иначе Telegram <cite index="20-1">отменяет транзакцию</cite>. Здесь стоит проверить, что товар еще существует, цена не изменилась и пользователь не пытается купить то, что уже куплено. Это фильтр перед списанием, а не подтверждение оплаты.
После pre_checkout_query Telegram обращается к платежному провайдеру, и если списание прошло, боту приходит сообщение с полем successful_payment. Только на этом этапе можно выдавать товар, начислять баланс или открывать доступ. В aiogram 3 это отдельный фильтр на тип контента:
from aiogram import F, Router
from aiogram.types import Message
router = Router()
@router.message(F.successful_payment)
async def process_successful_payment(message: Message):
payment = message.successful_payment
charge_id = payment.telegram_payment_charge_id
provider_charge_id = payment.provider_payment_charge_id
amount = payment.total_amount # в копейках
# дальше идемпотентная запись в базу, см. раздел нижеОтдельно поле telegram_payment_charge_id стоит сохранять сразу. По нему потом делается возврат средств и сверка с провайдером при спорных ситуациях.

ЮKassa рекомендует проверять уведомление по IP-адресу отправителя и повторным запросом статуса объекта через API, а не доверять телу webhook напрямую.
Если бот принимает оплату не через встроенные Telegram Payments, а напрямую через ЮKassa API с собственным notification_url, схема другая. Тут webhook это обычный публичный HTTP-эндпоинт, и его подделать проще, чем событие Bot API. Официальная документация ЮKassa советует <cite index="19-1">проверять подлинность уведомления по статусу объекта или по IP-адресу, чтобы защититься от атак с поддельными уведомлениями</cite>.

<cite index="19-1">ЮKassa может присылать уведомления с любого IP-адреса из фиксированного списка</cite>:

| Диапазон | Тип |
|---|---|
| 185.71.76.0/27 | IPv4 |
| 185.71.77.0/27 | IPv4 |
| 77.75.153.0/25 | IPv4 |
| 77.75.156.11 | IPv4 |
| 77.75.156.35 | IPv4 |
| 77.75.154.128/25 | IPv4 |
| 2a02:5180::/32 | IPv6 |
Запрос с любого другого адреса нужно отклонять до чтения тела. Если приложение стоит за прокси или балансировщиком, обязательно настроить trust proxy, иначе сервер будет видеть IP прокси вместо реального адреса ЮKassa, и проверка станет бессмысленной.
IP-фильтр не защищает от компрометации самой сети ЮKassa, поэтому второй шаг обязателен: после получения уведомления сделать собственный GET-запрос к API ЮKassa и получить актуальный статус платежа напрямую, а не верить полю status из тела webhook. Так подделка становится бессмысленной: даже если атакующий знает формат JSON, он не подделает ответ реального API по чужому id платежа.
import ipaddress
import httpx
TRUSTED_NETS = [
ipaddress.ip_network("185.71.76.0/27"),
ipaddress.ip_network("185.71.77.0/27"),
ipaddress.ip_network("77.75.153.0/25"),
ipaddress.ip_network("77.75.154.128/25"),
ipaddress.ip_network("2a02:5180::/32"),
]
async def handle_yookassa_webhook(request_ip: str, body: dict):
ip = ipaddress.ip_address(request_ip)
if not any(ip in net for net in TRUSTED_NETS):
return 403
payment_id = body["object"]["id"]
async with httpx.AsyncClient(auth=(SHOP_ID, SECRET_KEY)) as client:
resp = await client.get(f"https://api.yookassa.ru/v3/payments/{payment_id}")
payment = resp.json()
if payment["status"] != "succeeded":
return 200 # подтверждаем прием, но баланс не начисляем
# идемпотентно начисляем баланс, см. ниже
return 200Telegram и ЮKassa могут прислать одно и то же уведомление повторно. Без проверки на дубликат бот один раз спишет деньги у пользователя, а баланс начислит дважды.
И Bot API, и ЮKassa не гарантируют доставку события ровно один раз. Сеть моргнула, сервер ответил не 200, провайдер повторил доставку в течение 24 часов. Хендлер должен быть идемпотентным: перед начислением проверять, обрабатывался ли уже этот charge_id или payment_id.
Проще всего завести таблицу processed_payments с уникальным индексом на id платежа. Попытка вставить дубль упадет на уровне базы, и это станет естественной защитой, а не отдельным if-ом в коде бизнес-логики.

CREATE TABLE processed_payments (
payment_id TEXT PRIMARY KEY,
user_id BIGINT NOT NULL,
amount INTEGER NOT NULL,
processed_at TIMESTAMP DEFAULT now()
);Если INSERT прошел, баланс начисляем. Если словили ошибку уникальности, значит платеж уже учтен, и второй раз ничего не делаем. Ни отдельных флагов, ни блокировок Redis для этого не нужно.
Telegram Stars обрабатывается полностью на стороне Telegram, но проверка successful_payment и уникальности charge_id все равно обязательна.
Со звездами (XTR) все немного проще, потому что Telegram сам выступает платежным провайдером и внешний webhook не нужен. Но правило про successful_payment действует точно так же: список цен для инвойса должен состоять из одного элемента, а проверять факт оплаты нужно строго по этому событию, а не по факту, что пользователь нажал кнопку.
Отдельная тонкость Stars это возвраты. Метод RefundStarPayment принимает telegram_payment_charge_id, и повторный запрос на возврат для уже возвращенной транзакции нужно блокировать на своей стороне: <cite index="20-2">повторный запрос возврата для одной и той же транзакции должен быть заблокирован</cite>. Этот пункт есть в готовых примерах, но в спешке его часто вырезают.

| Событие | Что проверяет | Что делать боту |
|---|---|---|
| pre_checkout_query | Заказ актуален, товар доступен | Ответить answerPreCheckoutQuery за 10 секунд |
| successful_payment | Деньги реально списаны | Начислить баланс идемпотентно, сохранить charge_id |
| webhook ЮKassa | Событие с внешнего сервера | Проверить IP + перезапросить статус через API |
| RefundStarPayment | Повторный возврат | Проверить, не был ли charge_id уже возвращен |
Если провести оплату через встроенный provider_token ЮKassa в send_invoice, отдельный webhook не нужен, событие successful_payment уже проверено Telegram. Прямая интеграция с API ЮKassa требует собственной верификации webhook.
В большинстве гайдов на YouTube про подключение ЮKassa к боту на aiogram используется именно первый вариант: провайдер подключается через BotFather, а бот работает с send_invoice, pre_checkout_query и successful_payment без своего webhook-эндпоинта. Это заметно проще в защите, потому что вся проверка ложится на Telegram.
Прямая интеграция с API ЮKassa нужна, если бот продает что-то за пределами Telegram или требует функций, которых нет во встроенных платежах: например, чеков с гибкой настройкой или сплитования между несколькими продавцами. В этом случае весь раздел про IP и перезапрос статуса выше обязателен к реализации, без исключений.

Без проверки successful_payment или подлинности webhook бот можно заставить выдать товар или начислить баланс без реального платежа одним поддельным запросом.
Разберем на конкретных сценариях, что именно ломается.
| Сценарий пропуска проверки | Последствие |
|---|---|
| Баланс начисляется по нажатию кнопки, а не по successful_payment | Пользователь получает товар без оплаты |
| Webhook ЮKassa принимается без проверки IP | Атакующий шлет поддельный payment.succeeded с любым telegram-статусом |
| Нет проверки статуса через API ЮKassa | Даже валидный по формату webhook может быть подделан по содержимому |
| Нет идемпотентности по charge_id | Повторная доставка события удваивает баланс |
| Нет блокировки повторного refund | Пользователь запрашивает возврат несколько раз за одну транзакцию |
Каждая из этих дыр закрывается одной проверкой, но если пропустить хотя бы одну, вся цепочка защиты рассыпается. Это не гипотетический риск. Почти в каждом open-source примере бота с оплатой на GitHub отсутствует хотя бы один из пяти пунктов.

AI-инструменты вроде Cursor, Windsurf или Claude Code отлично справляются с рутинной частью: разметкой хендлеров, миграциями базы, версткой инвойса. Но код, который касается денег, стоит перечитывать самому, а не просто принимать diff.

Максим: «GoBanana принес 12 миллионов рублей, а собрали его за 6-8 часов вайб-кодинга. Но там, где заходят деньги, я всегда проверяю логику на сервере руками, а не верю тому, что написал клиент. Один раз довериться фронтенду, и баланс проекта можно обнулить одной поддельной командой».
Честная оговорка: даже если промпт явно просит "добавь проверку IP и идемпотентность", AI-модель может сгенерировать код, который выглядит правильно, но забывает про trust proxy или сравнивает IP без учета IPv6-диапазона. Платежный хендлер это тот редкий случай, когда стоит прогнать код через тесты с поддельным телом запроса и намеренно неправильным IP, прежде чем деплоить.
Если бот пишется через Cursor или Windsurf, для этого блока имеет смысл отдельно попросить модель написать unit-тест на отклонение чужого IP и на дублирующийся charge_id. Обе проверки легко забыть даже опытному разработчику. Каталог AI-инструментов для разработки собран на vibecoderz.ru/ide.

| Термин | Определение |
|---|---|
| Webhook | HTTP-эндпоинт на вашем сервере, на который платежная система шлет уведомления о событиях |
| pre_checkout_query | Событие Telegram Bot API перед списанием денег, требует ответа за 10 секунд |
| successful_payment | Событие Telegram Bot API после реального списания средств |
| provider_token | Токен платежного провайдера, привязанный к конкретному боту в BotFather |
| Идемпотентность | Свойство хендлера обрабатывать повторное событие без побочных эффектов |
| charge_id | Уникальный идентификатор транзакции, telegram_payment_charge_id или provider_payment_charge_id |
| IP whitelist | Список доверенных IP-адресов, с которых принимаются входящие запросы |
| Telegram Stars (XTR) | Внутренняя валюта Telegram для оплаты цифровых товаров внутри ботов |
Достаточно ли просто ответить 200 на webhook ЮKassa, чтобы считать его проверенным? Нет. Код 200 нужен только для подтверждения доставки, иначе ЮKassa будет повторять уведомление 24 часа. Подлинность проверяется отдельно, через IP и запрос статуса.
Можно ли начислять баланс сразу после pre_checkout_query? Нет, это только проверка заказа перед списанием. Официальная документация прямо предупреждает, что ответ на pre_checkout_query не гарантирует факт оплаты.
Что делать, если пользователь прислал скриншот об оплате в чат? Игнорировать как источник истины. Скриншот легко подделать, а бот должен ориентироваться только на successful_payment или проверенный webhook.
Нужно ли хранить telegram_payment_charge_id, если платеж уже обработан? Да, обязательно. Он нужен для возвратов и сверки с провайдером при спорах, без него служба поддержки не сможет доказать факт оплаты.
Отличается ли верификация для Telegram Stars и для ЮKassa через встроенные платежи? Нет, оба случая закрываются проверкой successful_payment и идемпотентностью по charge_id. Отдельный webhook и IP-фильтр нужны только при прямой интеграции с API ЮKassa.
Как протестировать защиту webhook перед продакшеном? Тестовый токен ЮKassa позволяет проводить платежи на сумму до 1000 рублей. Отдельно стоит вручную отправить запрос с чужого IP и с измененным телом, чтобы убедиться, что сервер их отклоняет.
Что если бот принимает платежи от нескольких провайдеров сразу? Тогда таблица processed_payments должна хранить не только id платежа, но и провайдера, а проверка IP настраивается индивидуально под каждого из них.
Если после этого гайда остались вопросы по конкретной архитектуре бота, можно разобрать ее на консультации с Максимом. Больше материалов по AI-инструментам для разработки в каталоге VibeCoderz.
Обновлено: август 2026.