Бот для Telegram с подпиской на канал - типовая задача для закрытых каналов, марафонов, розыгрышей и продажи доступа к контенту. Я собирал такую логику десятки раз для разных клиентов: от простого гейта «подпишись - получи чек-лист» до сложных сценариев с несколькими каналами и закрытыми группами. Разберу, как это устроено на уровне Bot API, и покажу рабочий код на aiogram, который можно брать за основу.
Зачем проверять подписку на канал в боте
Самый частый кейс из моей практики - лид-магнит. Пользователь пишет боту /start, получает предложение подписаться на канал, после подписки бот выдаёт PDF-гайд, промокод или доступ к закрытой части контента. Без проверки часть аудитории подписывается, забирает бонус и через день отписывается - метрика удержания канала превращается в пыль.
Второй кейс - розыгрыши и марафоны, где организатор обязан подтвердить, что участник реально состоит в канале, а не просто нажал кнопку «участвую». Третий - платные закрытые каналы, где бот выполняет роль привратника: без активной подписки не открывает доступ к материалам или следующему уроку.
Во всех этих сценариях логика одна: бот дергает Telegram Bot API, получает статус пользователя в канале и на основе этого статуса решает, показывать контент или отправлять на подписку.
Метод getChatMember и статусы участников канала
Проверка держится на одном методе Bot API - getChatMember. Он принимает chat_id канала (username вида @channel или числовой id, если канал приватный) и user_id проверяемого пользователя, а возвращает объект ChatMember со статусом.
Статусы, с которыми реально приходится работать:
| Статус | Что значит |
|---|---|
| creator | владелец канала |
| administrator | админ канала |
| member | обычный подписчик |
| restricted | участник с ограничениями (актуально для групп) |
| left | вышел или не подписан |
| kicked | заблокирован в канале |
Подписанным я в коде считаю пользователя со статусом creator, administrator или member. Всё остальное - доступ закрыт.
Обязательное условие, о которое спотыкаются почти все новички: бот должен быть добавлен в канал администратором. Без этого getChatMember для приватного канала вернёт ошибку доступа, а для публичного может отработать нестабильно в зависимости от настроек канала.
Бесплатный материал
🎁 Полезный скрипт в подарок
Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.
Без спама. Отписка в 1 клик.
Проверка подписки в боте на aiogram: рабочий пример
Показываю кусок кода на aiogram 3.x - так я обычно закладываю проверку в новых ботах для клиентов.
from aiogram import Bot, Dispatcher, F
from aiogram.filters import CommandStart
from aiogram.types import (
Message, CallbackQuery,
InlineKeyboardMarkup, InlineKeyboardButton,
)
CHANNEL_ID = "@kalinkindev_channel"
async def is_subscribed(bot: Bot, user_id: int) -> bool:
try:
member = await bot.get_chat_member(chat_id=CHANNEL_ID, user_id=user_id)
return member.status in ("member", "administrator", "creator")
except Exception:
return False
def subscribe_keyboard() -> InlineKeyboardMarkup:
return InlineKeyboardMarkup(inline_keyboard=[
[InlineKeyboardButton(text="Подписаться", url="https://t.me/kalinkindev_channel")],
[InlineKeyboardButton(text="Я подписался", callback_data="check_sub")],
])
async def start_handler(message: Message, bot: Bot):
if await is_subscribed(bot, message.from_user.id):
await message.answer("Доступ открыт, добро пожаловать!")
else:
await message.answer(
"Чтобы продолжить, подпишитесь на канал",
reply_markup=subscribe_keyboard(),
)
async def check_sub_handler(call: CallbackQuery, bot: Bot):
if await is_subscribed(bot, call.from_user.id):
await call.message.edit_text("Подписка подтверждена, доступ открыт")
else:
await call.answer("Пока не вижу подписку, попробуйте ещё раз", show_alert=True)
dp = Dispatcher()
dp.message.register(start_handler, CommandStart())
dp.callback_query.register(check_sub_handler, F.data == "check_sub")
Функция is_subscribed - это и есть вся логика проверки. Дальше её можно вызывать в любом хендлере: перед выдачей файла, перед показом меню, в middleware для всего бота сразу.
Готовый каркас с такой проверкой и парой смежных фич я выкладываю в библиотеке готовых скриптов - можно забрать как отправную точку и не собирать структуру бота с нуля.
Кнопка «Я подписался» и повторная проверка без перезапуска
Плохой UX - заставлять пользователя заново набирать /start после подписки. Правильно - кнопка «Я подписался» с callback_data, которая триггерит повторный вызов getChatMember и меняет текст того же сообщения через edit_text, без нового сообщения в чате.
Если проверка не прошла, я не переписываю сообщение целиком, а показываю всплывающее уведомление через call.answer(text, show_alert=True) - так пользователь видит, что бот отреагировал на нажатие, а исходное сообщение с кнопками остаётся на месте.
Отдельный момент - защита от спам-кликов. Если пользователь долбит кнопку раз в секунду, каждый клик - это отдельный запрос к API. На проектах с большой активной аудиторией я ставлю простой троттлинг: храню в памяти (или в Redis, если инстансов бота несколько) время последней проверки конкретного пользователя и игнорирую повторные клики чаще, чем раз в 3-5 секунд.
Ограничения API и типичные ошибки проверки подписки
С чем реально сталкиваешься на проде:
- Bad Request: user not found - пользователь никогда не взаимодействовал с ботом или каналом, Telegram ещё не «видит» его в контексте этого чата. Актуально в основном для сценариев, где id пользователя пробуют получить из внешнего источника, а не из апдейта самого бота.
- Forbidden: bot is not a member of the channel - бота удалили из админов канала после запуска. Проверяю это на старте бота и логирую отдельно, чтобы не тратить время на дебаг основной логики.
- Лимиты API - Telegram ограничивает бота примерно 30 запросами в секунду суммарно и 1 сообщением в секунду на один чат.
getChatMemberсчитается в общий лимит, и на аудитории от 10-15 тысяч человек прямая проверка при каждом действии пользователя быстро упрётся в троттлинг со стороны Telegram.
Для крупных ботов я кэширую результат проверки на 5-10 минут (обычный словарь с TTL или Redis) и полагаюсь на него между реальными обращениями к API. Для событий в реальном времени есть альтернатива - подписка на апдейты chat_member, которые Telegram присылает боту сам при вступлении или выходе пользователя из канала, без необходимости дергать getChatMember вручную по каждому действию.
| Способ | Когда срабатывает | Нагрузка на API |
|---|---|---|
| Прямой вызов getChatMember по клику | сразу, по требованию пользователя | высокая при большой аудитории |
| Кэш с TTL 5-10 минут | с задержкой до истечения кэша | низкая |
| Подписка на chat_member апдейты | в момент реального события в канале | минимальная, без опроса |
Проверка нескольких каналов, закрытых групп и автоматизация в n8n
Если каналов несколько, я просто прогоняю пользователя по списку и требую member-статус в каждом:
CHANNELS = ["@kalinkindev_channel", "@kalinkindev_chat"]
async def is_subscribed_all(bot: Bot, user_id: int) -> bool:
for chat_id in CHANNELS:
member = await bot.get_chat_member(chat_id=chat_id, user_id=user_id)
if member.status not in ("member", "administrator", "creator"):
return False
return True
С закрытыми группами метод работает точно так же, важно только заранее знать числовой chat_id - у приватных групп и каналов нет публичного username, id придётся получить через getChat или из апдейта, когда бот только добавляется в чат.
Отдельный сценарий - закрытые каналы со вступлением по заявке (join request). Там вместо прямой проверки подписки бот ловит апдейт chat_join_request и вызывает approveChatJoinRequest или declineChatJoinRequest - это уже не проверка существующей подписки, а модерация самого процесса вступления, и логика в боте строится иначе.
Когда проверка подписки - только часть воронки, я обычно выношу её результат за пределы бота: по факту успешной подписки бот бьёт вебхуком в n8n, а дальше сценарий в n8n обновляет карточку в CRM, ставит тег пользователю или запускает цепочку сообщений. Для хранения контактов подписчиков в такой связке важно держать базу на сервере в РФ, а не в иностранном сервисе - этого требует 152-ФЗ, если бот работает с российской аудиторией. Настройка такой связки с ботом и n8n - отдельная задача, для готового сценария под конкретный проект я делаю автоматизацию в n8n от 25 000 ₽.
Автоматизация в мессенджере
Telegram-бот / Mini App
от 30 000 ₽
Подробнее →Частые вопросы
Нужно ли делать бота администратором канала для проверки подписки?
Да, для приватных каналов это обязательное условие - без прав администратора getChatMember вернёт ошибку доступа. Для публичных каналов бот тоже должен состоять в канале хотя бы как участник, иначе Telegram может отдавать нестабильный результат в зависимости от настроек приватности канала.
Можно ли проверить подписку без Bot API, просто по ссылке на канал?
Нет, надёжного способа снаружи Bot API не существует - количество подписчиков в публичном виде ничего не говорит о конкретном пользователе. Единственный рабочий путь - вызов getChatMember с id канала и id пользователя внутри самого бота.
Сколько стоит разработка бота с проверкой подписки на канал?
У меня разработка телеграм-бота начинается от 30 000 ₽ - цена зависит от того, нужна ли только проверка подписки или ещё интеграция с CRM, оплатой, несколькими каналами и автоматизацией. Точную оценку под конкретный сценарий даю на консультации, она стоит от 3 000 ₽.
Как часто нужно перепроверять подписку у уже пропущенных пользователей?
Зависит от ценности доступа. Для разового лид-магнита достаточно проверки один раз в момент выдачи. Для закрытого платного канала я обычно ставлю периодическую перепроверку раз в сутки через фоновую задачу или полагаюсь на апдейты chat_member, которые Telegram присылает сам при выходе пользователя из канала.