Chat join requests — это режим закрытого канала или группы, где пользователь не подписывается мгновенно, а отправляет заявку, которую админ или бот одобряет. Для маркетинга это золотая фича: сам факт подачи заявки трактуется Telegram как разрешение на DM от бота, и вы можете сразу написать новому подписчику в личку с лид-магнитом.
Разберём, как включить заявки, как обрабатывать chat_join_request в Bot API, как фильтровать ботов и как связать всё с CRM-воронкой.
Зачем нужны заявки
В обычном режиме «открытая ссылка-приглашение» подписчики идут потоком, и среди них 10–30% — боты, фейки и фрод. В режиме заявок:
- бот сразу видит, кто подал заявку (
from_user); - может проверить юзера по антифрод-критериям;
- может задать вопрос или показать оффер до одобрения;
- получает разрешение писать в DM этому юзеру;
- админы видят чистую аудиторию без чисток постфактум.
Минус один — конверсия из «увидел канал» в «подписан» падает на 15–25% за счёт лишнего шага «подать заявку → ждать».
Включение заявок
В настройках канала или группы → Invite Links → создать ссылку с галкой Request Admin Approval. Старая ссылка-приглашение продолжит работать без заявок — нужно её удалить или сменить.
Программно через бота:
link = await bot.create_chat_invite_link(
chat_id=CHANNEL_ID,
name="Реклама в Я.Директ",
creates_join_request=True,
)
print(link.invite_link) # https://t.me/+abc123...
Имя ссылки удобно для атрибуции: разные источники трафика — разные ссылки.
Обработка chat_join_request
Бот должен быть админом канала с правом invite_users (или manage_chat). Подпишитесь на апдейт в setWebhook:
await bot.set_webhook(
url=URL,
secret_token=SECRET,
allowed_updates=["message", "chat_join_request", "callback_query"],
)
Хендлер:
from aiogram import Router, F
from aiogram.types import ChatJoinRequest
router = Router()
@router.chat_join_request(F.chat.id == CHANNEL_ID)
async def on_join_request(req: ChatJoinRequest, bot: Bot):
user = req.from_user
# 1. Антифрод
if await is_likely_bot(user):
await bot.decline_chat_join_request(req.chat.id, user.id)
return
# 2. Одобрение
await bot.approve_chat_join_request(req.chat.id, user.id)
# 3. DM с лид-магнитом
try:
await bot.send_message(
user.id,
f"Привет, {user.first_name}! Спасибо за подписку. Держи чек-лист:",
reply_markup=lead_magnet_kb(),
)
except TelegramForbiddenError:
pass # юзер запретил DM
Антифрод-критерии
Простейший набор проверок, отсеивающий 80% ботов:
| Критерий | Признак бота |
|---|---|
| Возраст аккаунта | менее 7 дней (user.id < некоторого порога) |
| Username | пустой или [a-z]{4}\d{6} |
| Аватар | отсутствует |
| Premium | редко у ботов |
| Язык | не из ожидаемого списка |
| История | ноль сообщений в общих чатах |
async def is_likely_bot(user) -> bool:
if user.is_bot:
return True
if not user.username and user.first_name in ("User", "Telegram"):
return True
photos = await bot.get_user_profile_photos(user.id, limit=1)
if photos.total_count == 0 and user.id > 7_000_000_000:
return True
return False
Юзер-ID > 7B — признак свежесозданного аккаунта (в 2026 году нумерация перешла за этот порог недавно).
Каптча в DM как доп-фильтр
Для жёсткой фильтрации можно сначала отправить в DM каптчу, и одобрить заявку только после успешного решения:
@router.chat_join_request(F.chat.id == CHANNEL_ID)
async def on_join(req: ChatJoinRequest, bot: Bot):
sent = await bot.send_message(
req.from_user.id,
"Чтобы одобрить заявку, нажмите кнопку ниже в течение 5 минут.",
reply_markup=captcha_kb(),
)
await pending.put(req.from_user.id, req.chat.id, expires=300)
@router.callback_query(F.data == "captcha:ok")
async def captcha_ok(cb: CallbackQuery, bot: Bot):
pending_req = await pending.get(cb.from_user.id)
if not pending_req:
return
await bot.approve_chat_join_request(pending_req.chat_id, cb.from_user.id)
await cb.answer("Заявка одобрена!")
Боты, которые не прошли DM (запретили его), отсеиваются автоматически — send_message бросит исключение, и вы их не одобрите.
Связка с CRM
Каждая одобренная заявка — потенциальный лид. Шлите в CRM с источником из имени invite-ссылки:
async def push_lead(user, invite_link_name: str):
await crm.create_lead({
"name": user.full_name,
"telegram_id": user.id,
"username": user.username,
"source": invite_link_name, # "Реклама в Я.Директ"
"status": "subscribed",
})
В CRM сразу строится отчёт «подписки за период по источнику», а у менеджера появляется задача написать лиду через 24 часа.
Лимиты и подводные
approve_chat_join_requestиdecline_chat_join_request— мягкие лимиты ~30 в секунду на бота.- Если не одобрить и не отклонить заявку, она висит до 60 дней, потом автоматически отклоняется.
chat_join_requestприходит только если бот админ с правом приглашать.- В супергруппах с включёнными заявками работает то же API.
- Юзер может отозвать заявку до одобрения — в Bot API такого апдейта нет, узнать об этом не получится.
Аналитика заявок
Полезные метрики:
| Метрика | Как считать |
|---|---|
| Подано заявок | счётчик chat_join_request |
| Одобрено | счётчик approve_chat_join_request |
| Отклонено антифродом | разница |
| % с DM | сколько успешно получили send_message |
| Конверсия в платёж | join → /start → payment |
| Стоимость подписчика | бюджет / одобренных |
Топ-5 ошибок
- Бот не подписан на
chat_join_requestвallowed_updates— апдейты не приходят. - Не обрабатывают
TelegramForbiddenErrorпри отправке DM — крашится весь хендлер. - Одобряют всех подряд — канал засоряется ботами.
- Не различают
invite_link.name— нет атрибуции трафика. - Забывают, что
chat_join_requestтоже подпадает под лимит 30 запросов/сек.
Итого
Chat join requests превращают подписку в управляемый процесс: вы фильтруете ботов, ловите DM-разрешение, размечаете трафик и сразу шлёте в CRM. Это +30–40% к качеству базы и х2 к конверсии в платящего клиента по сравнению с открытой ссылкой. Включайте заявки на любых каналах от 1000 подписчиков и обязательно подключайте бота для автоматизации.
Частые вопросы
Можно ли одобрять заявки массово?
Да, циклом по всем pending-заявкам. Telegram не отдаёт список через Bot API — нужно ловить chat_join_request в момент подачи и складывать в свою очередь. Альтернатива — клиент-сессия (Telethon) с методом messages.GetChatInviteImporters, который возвращает все ожидающие заявки.
Сколько заявок могут висеть одновременно?
Жёсткого лимита нет, но через 60 дней без действия Telegram автоматически отклоняет заявку. На практике 50–100 тысяч pending-заявок — рабочий объём, после этого UI админа начинает тормозить.
Что если юзер запретил DM?
send_message вернёт Forbidden: bot can't initiate conversation. Заявку всё равно одобряем (запретов на это нет), просто помечаем в CRM dm_blocked: true. Такие юзеры всё ещё подписаны на канал и видят посты.
Как проверить, что заявка пришла именно с конкретной ссылки?
В объекте ChatJoinRequest есть поле invite_link с заголовком и именем ссылки. Сравнивайте invite_link.name со своим списком источников. Это и даёт атрибуцию: «эта подписка — с креатива №42 в Telegram Ads».
Нужно ли спрашивать согласие на DM отдельно?
Технически нет — подача заявки = разрешение. Юридически по 152-ФЗ для маркетинговых рассылок нужно явное согласие на обработку ПД и рекламу. Поэтому первое DM делайте сервисным («одобрено, добро пожаловать»), а перед прогревной серией явно спрашивайте «разрешите присылать вам полезные материалы?» с кнопкой согласия.
Можно ли автоматически отклонять заявки без аватара?
Можно, но это даст ложноположительные срабатывания у 5–10% реальных юзеров (особенно у новичков в Telegram). Лучше комбинировать признаки: нет аватара + нет username + аккаунт младше 30 дней — отклоняем; иначе одобряем.
Как сегментировать аудиторию по разным invite-ссылкам?
Создайте по одной createChatInviteLink на каждый источник трафика с уникальным name. В обработчике chat_join_request берите invite_link.name и кладите в профиль юзера. Дальше воронка автоматически идёт через нужный сценарий — у каждого источника может быть своя цепочка прогрева.