Безопасность платежей в Telegram-боте держится на одном простом принципе: серверу нельзя верить тому, что прислал клиент. Если бот засчитывает оплату по сообщению из фронтенда, кнопке или локальному стейту приложения, у него можно увести баланс за пять минут без единого рубля на счете ЮKassa. Ниже разберем пошагово, как проверять webhook ЮKassa и событие successful_payment на сервере, а не доверять глазам пользователя.
Главный вывод: доверять можно только серверным событиям от Telegram и ЮKassa, проверенным по IP и статусу объекта. ЮKassa присылает уведомления с фиксированного списка IP-адресов, а Telegram шлет successful_payment только после реального списания. Ниже показан код для aiogram, таблица проверок и разбор частых дыр в готовых гайдах на YouTube.

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

Как Telegram Bot API на самом деле подтверждает оплату?
Telegram присылает два события: pre_checkout_query перед списанием и successful_payment после него. Только второе означает, что деньги реально ушли.
Официальная документация Bot Payments API прямо предупреждает: нужно всегда дожидаться <cite index="21-1">события successful_payment перед выдачей товара, потому что сам по себе ответ на pre_checkout_query еще не гарантирует успешный заказ или платеж</cite>. Это ключевая деталь, которую пропускают в половине туториалов на YouTube.
Что делает pre_checkout_query?
Когда пользователь жмет кнопку оплаты в инвойсе, Telegram шлет боту pre_checkout_query с полными данными о заказе. Бот обязан ответить методом answerPreCheckoutQuery в течение 10 секунд, иначе Telegram <cite index="20-1">отменяет транзакцию</cite>. Здесь стоит проверить, что товар еще существует, цена не изменилась и пользователь не пытается купить то, что уже куплено. Это фильтр перед списанием, а не подтверждение оплаты.
Что делает successful_payment?
После 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 стоит сохранять сразу. По нему потом делается возврат средств и сверка с провайдером при спорных ситуациях.

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

Проверка по IP-адресу
<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 200Как избежать двойной обработки платежа?
Telegram и Ю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 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 уже возвращен |
ЮKassa через Telegram Payments или напрямую по API, что безопаснее?
Если провести оплату через встроенный 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 для оплаты цифровых товаров внутри ботов |
Частые вопросы про безопасность платежей в 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.