VibeCoderzVibeCoderz
Все статьи
2026/08/047 мин чтения

Безопасность платежей в Telegram-боте ЮKassa и Stars 2026

Безопасность платежей в Telegram-боте держится на одном простом принципе: серверу нельзя верить тому, что прислал клиент. Если бот засчитывает оплату по сообщению из фронтенда, кнопке или локальному стейту приложения, у него можно увести баланс за пя…

Содержание (10)+

Безопасность платежей в 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/27IPv4
185.71.77.0/27IPv4
77.75.153.0/25IPv4
77.75.156.11IPv4
77.75.156.35IPv4
77.75.154.128/25IPv4
2a02:5180::/32IPv6

Запрос с любого другого адреса нужно отклонять до чтения тела. Если приложение стоит за прокси или балансировщиком, обязательно настроить 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.

Глоссарий

Изображение
ТерминОпределение
WebhookHTTP-эндпоинт на вашем сервере, на который платежная система шлет уведомления о событиях
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.

All Posts

Автор

Елисавета Наговицына
Елисавета Наговицына

Предприниматель · Контент-маркетолог · SEO-стратег · AI-продуктолог

2026/08/04

400 000+ органических переходов за 3 месяца. Со-основатель GoBanana (231K пользователей, 12+ млн ₽ без рекламы) и NeuroScribe (65K пользователей). SEO/GEO-стратегии для AI-поисковиков, 1 700+ единиц контента, 17+ реализованных стратегий.

Об авторе →

Читать далее

📢 Новость

Claude Code: новый CLI-агент от Anthropic

Anthropic выпустила Claude Code — терминальный AI-агент для разработчиков. Инструмент работает прямо в командной строке и умеет писать, редактировать и запускать код.

2026/02/27
📝 Конспект

Zcode AI: Полный гид по визуальному интерфейсу для Claude Code и AI-агентов

Узнайте, как использовать Zcode для управления Claude Code, Gemini и Codex в едином GUI. Настройка провайдеров, MCP-серверов и визуальный вайбкодинг.

2026/02/28
📝 Конспект

YouTube-канал с монетизацией из любой точки мира: Пошаговый гайд 2026

Инструкция по созданию YouTube-канала: обход блокировок SMS, настройка расширенных функций через виртуальные номера и правила безопасности для монетизации.

2026/02/28
📝 Конспект

Windsurf Code Maps: Как глубоко понимать архитектуру проекта перед написанием кода

Полный гайд по Windsurf Code Maps, модели Sway 1.5 и Sway Grep. Узнайте, как визуализировать архитектуру кода и ускорить разработку в 13 раз.

2026/02/28
📝 Конспект

Vk Fast Cash Strategy

Аудитория ВКонтакте — это те же люди, что и в Instagram, но 'социальный контракт' площадки другой. Если Instagram — это 'дорогой ресторан' с демонстрацией успеха, то VK — это 'душевная шашлычная'. Здесь не работает глянцевый 'успешный успех

2026/02/28